Skip to main content

devforge/
lib.rs

1mod config;
2mod docker;
3pub mod env;
4mod runner;
5
6pub use config::Config;
7
8use std::fs;
9use std::process;
10
11/// Entry point for consumer xtask binaries. Reads `devforge.toml` from the
12/// workspace root and dispatches the subcommand.
13pub fn run() {
14    let root = env::workspace_root();
15    let toml_path = root.join("devforge.toml");
16    let toml_str = fs::read_to_string(&toml_path).unwrap_or_else(|e| {
17        env::fatal(&format!("Failed to read {}: {e}", toml_path.display()));
18    });
19    let config = Config::load(&toml_str).unwrap_or_else(|e| {
20        env::fatal(&format!("Failed to parse devforge.toml: {e}"));
21    });
22
23    let args: Vec<String> = std::env::args().skip(1).collect();
24    match args.first().map(|s| s.as_str()) {
25        Some("dev") => cmd_dev(&root, &config),
26        Some("infra") => cmd_infra(&root, &config),
27        Some(name) => {
28            if let Some(cmd) = config.commands.iter().find(|c| c.name == name) {
29                cmd_custom(&root, &config, cmd);
30            } else {
31                eprintln!("Unknown command: {name}\n");
32                print_usage(&config);
33                process::exit(1);
34            }
35        }
36        None => {
37            print_usage(&config);
38            process::exit(1);
39        }
40    }
41}
42
43fn preflight(root: &std::path::Path, config: &Config) {
44    for file in &config.env_files {
45        env::check_file(&root.join(file));
46    }
47
48    // Load .env files (files without extension or with .env extension)
49    for env_file in &config.env_files {
50        let path = root.join(env_file);
51        if path.exists() {
52            let is_dotenv = path.extension().is_none()
53                || path.extension().is_some_and(|e| e == "env");
54            if is_dotenv {
55                env::load_dotenv(&path);
56            }
57        }
58    }
59
60    for tool in &config.required_tools {
61        env::require_cmd(tool);
62    }
63}
64
65fn cmd_dev(root: &std::path::Path, config: &Config) {
66    preflight(root, config);
67
68    docker::compose_up(root, &config.docker);
69    docker::wait_for_health(root, &config.docker.health_checks);
70    runner::run_hooks(root, &config.dev.hooks);
71
72    let status = runner::launch_runner(root, &config.dev);
73
74    docker::compose_down(root, &config.docker);
75
76    if let Some(status) = status {
77        match status.code() {
78            Some(0) | None => {}
79            Some(code) => process::exit(code),
80        }
81    }
82}
83
84fn cmd_infra(root: &std::path::Path, config: &Config) {
85    preflight(root, config);
86
87    docker::compose_up(root, &config.docker);
88    docker::wait_for_health(root, &config.docker.health_checks);
89
90    env::log("Infrastructure running. Press Ctrl+C to stop.");
91
92    let (tx, rx) = std::sync::mpsc::channel();
93    ctrlc::set_handler(move || {
94        let _ = tx.send(());
95    })
96    .expect("failed to set Ctrl+C handler");
97    let _ = rx.recv();
98
99    docker::compose_down(root, &config.docker);
100}
101
102fn cmd_custom(root: &std::path::Path, config: &Config, cmd: &config::CustomCommand) {
103    preflight(root, config);
104
105    if cmd.docker {
106        docker::compose_up(root, &config.docker);
107        docker::wait_for_health(root, &config.docker.health_checks);
108    }
109
110    runner::run_custom(root, cmd);
111
112    if cmd.docker {
113        docker::compose_down(root, &config.docker);
114    }
115}
116
117fn print_usage(config: &Config) {
118    eprintln!("Usage: cargo xtask <command>\n");
119    eprintln!("Commands:");
120    eprintln!("  dev     Start all services in mprocs TUI");
121    eprintln!("  infra   Start Docker infrastructure only");
122    for cmd in &config.commands {
123        eprintln!("  {:<7} {}", cmd.name, cmd.description);
124    }
125}