mod config;
mod docker;
pub mod env;
mod runner;
pub use config::Config;
use std::fs;
use std::process;
pub fn run() {
let root = env::workspace_root();
let toml_path = root.join("devforge.toml");
let toml_str = fs::read_to_string(&toml_path).unwrap_or_else(|e| {
env::fatal(&format!("Failed to read {}: {e}", toml_path.display()));
});
let config = Config::load(&toml_str).unwrap_or_else(|e| {
env::fatal(&format!("Failed to parse devforge.toml: {e}"));
});
let args: Vec<String> = std::env::args().skip(1).collect();
match args.first().map(|s| s.as_str()) {
Some("dev") => cmd_dev(&root, &config),
Some("infra") => cmd_infra(&root, &config),
Some(name) => {
if let Some(cmd) = config.commands.iter().find(|c| c.name == name) {
cmd_custom(&root, &config, cmd);
} else {
eprintln!("Unknown command: {name}\n");
print_usage(&config);
process::exit(1);
}
}
None => {
print_usage(&config);
process::exit(1);
}
}
}
fn preflight(root: &std::path::Path, config: &Config) {
for entry in &config.env_files {
if let Some(template) = &entry.template {
let target = root.join(&entry.path);
if !target.exists() {
let src = root.join(template);
if !src.exists() {
env::fatal(&format!(
"Template not found: {} (for {})",
src.display(),
entry.path,
));
}
fs::copy(&src, &target).unwrap_or_else(|e| {
env::fatal(&format!(
"Failed to copy {} \u{2192} {}: {e}",
src.display(),
target.display(),
));
});
env::log(&format!(
"Copied {} \u{2192} {}",
template, entry.path,
));
}
}
}
for entry in &config.env_files {
env::check_file(&root.join(&entry.path));
}
for entry in &config.env_files {
let path = root.join(&entry.path);
if path.exists() {
let is_dotenv = path.extension().is_none()
|| path.extension().is_some_and(|e| e == "env");
if is_dotenv {
env::load_dotenv(&path);
}
}
}
for tool in &config.required_tools {
env::require_cmd(tool);
}
}
fn cmd_dev(root: &std::path::Path, config: &Config) {
preflight(root, config);
docker::compose_up(root, &config.docker);
docker::wait_for_health(root, &config.docker.health_checks);
runner::run_hooks(root, &config.dev.hooks);
let status = runner::launch_runner(root, &config.dev);
docker::compose_down(root, &config.docker);
if let Some(status) = status {
match status.code() {
Some(0) | None => {}
Some(code) => process::exit(code),
}
}
}
fn cmd_infra(root: &std::path::Path, config: &Config) {
preflight(root, config);
docker::compose_up(root, &config.docker);
docker::wait_for_health(root, &config.docker.health_checks);
env::log("Infrastructure running. Press Ctrl+C to stop.");
let (tx, rx) = std::sync::mpsc::channel();
ctrlc::set_handler(move || {
let _ = tx.send(());
})
.expect("failed to set Ctrl+C handler");
let _ = rx.recv();
docker::compose_down(root, &config.docker);
}
fn cmd_custom(root: &std::path::Path, config: &Config, cmd: &config::CustomCommand) {
preflight(root, config);
if cmd.docker {
docker::compose_up(root, &config.docker);
docker::wait_for_health(root, &config.docker.health_checks);
}
runner::run_custom(root, cmd);
if cmd.docker {
docker::compose_down(root, &config.docker);
}
}
fn print_usage(config: &Config) {
eprintln!("Usage: cargo xtask <command>\n");
eprintln!("Commands:");
eprintln!(" dev Start all services in mprocs TUI");
eprintln!(" infra Start Docker infrastructure only");
for cmd in &config.commands {
eprintln!(" {:<7} {}", cmd.name, cmd.description);
}
}