1mod config;
2mod docker;
3pub mod env;
4mod runner;
5
6pub use config::Config;
7
8use std::fs;
9use std::process;
10
11pub 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 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}