Skip to main content

ferro_cli/commands/
docker_compose.rs

1//! docker:compose command - Generate docker-compose.yml for local development
2
3use console::style;
4use dialoguer::{theme::ColorfulTheme, Confirm};
5use std::fs;
6use std::path::Path;
7use toml::Value;
8
9use crate::templates;
10
11pub fn run(with_mailpit: bool, with_minio: bool) {
12    // Verify we're in a Ferro project directory
13    if !Path::new("Cargo.toml").exists() {
14        eprintln!("{} Cargo.toml not found", style("Error:").red().bold());
15        eprintln!(
16            "{}",
17            style("Make sure you're in a Ferro project root directory.").dim()
18        );
19        std::process::exit(1);
20    }
21
22    // Get project name
23    let project_name = get_project_name();
24
25    let compose_path = Path::new("docker-compose.yml");
26
27    // Check if docker-compose.yml already exists
28    if compose_path.exists() {
29        eprintln!(
30            "{} docker-compose.yml already exists",
31            style("Info:").yellow().bold()
32        );
33        eprintln!(
34            "{}",
35            style("Remove or rename the existing docker-compose.yml to generate a new one.").dim()
36        );
37        std::process::exit(0);
38    }
39
40    // Prompt for optional services
41    let (include_mailpit, include_minio) = prompt_for_services(with_mailpit, with_minio);
42
43    // Generate docker-compose.yml
44    let compose_content =
45        templates::docker_compose_template(&project_name, include_mailpit, include_minio);
46    if let Err(e) = fs::write(compose_path, compose_content) {
47        eprintln!(
48            "{} Failed to write docker-compose.yml: {}",
49            style("Error:").red().bold(),
50            e
51        );
52        std::process::exit(1);
53    }
54    println!("{} Created docker-compose.yml", style("✓").green());
55
56    // Update .gitignore if needed
57    update_gitignore();
58
59    // Print usage instructions
60    print_instructions(&project_name, include_mailpit, include_minio);
61}
62
63fn get_project_name() -> String {
64    let cargo_toml = match fs::read_to_string("Cargo.toml") {
65        Ok(content) => content,
66        Err(_) => {
67            // Fallback to directory name
68            return std::env::current_dir()
69                .ok()
70                .and_then(|p| p.file_name().map(|s| s.to_string_lossy().to_string()))
71                .unwrap_or_else(|| "ferro_app".to_string());
72        }
73    };
74
75    let parsed: Value = match cargo_toml.parse() {
76        Ok(v) => v,
77        Err(_) => {
78            return std::env::current_dir()
79                .ok()
80                .and_then(|p| p.file_name().map(|s| s.to_string_lossy().to_string()))
81                .unwrap_or_else(|| "ferro_app".to_string());
82        }
83    };
84
85    parsed["package"]["name"]
86        .as_str()
87        .unwrap_or("ferro_app")
88        .to_string()
89}
90
91fn prompt_for_services(with_mailpit: bool, with_minio: bool) -> (bool, bool) {
92    // If flags are provided, use them directly
93    if with_mailpit || with_minio {
94        return (with_mailpit, with_minio);
95    }
96
97    println!();
98    println!("{}", style("Optional Services").cyan().bold());
99    println!(
100        "{}",
101        style("PostgreSQL and Redis are included by default.").dim()
102    );
103    println!();
104
105    let include_mailpit = Confirm::with_theme(&ColorfulTheme::default())
106        .with_prompt("Include Mailpit (email testing)?")
107        .default(false)
108        .interact()
109        .unwrap_or(false);
110
111    let include_minio = Confirm::with_theme(&ColorfulTheme::default())
112        .with_prompt("Include MinIO (S3-compatible storage)?")
113        .default(false)
114        .interact()
115        .unwrap_or(false);
116
117    println!();
118
119    (include_mailpit, include_minio)
120}
121
122fn update_gitignore() {
123    let gitignore_path = Path::new(".gitignore");
124    if !gitignore_path.exists() {
125        return;
126    }
127
128    let content = match fs::read_to_string(gitignore_path) {
129        Ok(c) => c,
130        Err(_) => return,
131    };
132
133    // Check if docker-compose.override.yml is already ignored
134    if content.contains("docker-compose.override.yml") {
135        return;
136    }
137
138    // Append to .gitignore
139    let new_content = format!(
140        "{}\n# Local Docker overrides\ndocker-compose.override.yml\n",
141        content.trim_end()
142    );
143
144    if fs::write(gitignore_path, new_content).is_ok() {
145        println!("{} Updated .gitignore", style("✓").green());
146    }
147}
148
149fn print_instructions(project_name: &str, has_mailpit: bool, has_minio: bool) {
150    println!();
151    println!(
152        "{}",
153        style("Docker Compose created successfully!").cyan().bold()
154    );
155    println!();
156    println!("Start services:");
157    println!("  {}", style("docker compose up -d").cyan());
158    println!();
159    println!("Stop services:");
160    println!("  {}", style("docker compose down").cyan());
161    println!();
162    println!("Services:");
163    println!(
164        "  {} PostgreSQL: {}",
165        style("•").dim(),
166        style("localhost:5432").underlined()
167    );
168    println!(
169        "  {} Redis: {}",
170        style("•").dim(),
171        style("localhost:6379").underlined()
172    );
173    if has_mailpit {
174        println!(
175            "  {} Mailpit SMTP: {}",
176            style("•").dim(),
177            style("localhost:1025").underlined()
178        );
179        println!(
180            "  {} Mailpit UI: {}",
181            style("•").dim(),
182            style("http://localhost:8025").underlined()
183        );
184    }
185    if has_minio {
186        println!(
187            "  {} MinIO API: {}",
188            style("•").dim(),
189            style("localhost:9000").underlined()
190        );
191        println!(
192            "  {} MinIO Console: {}",
193            style("•").dim(),
194            style("http://localhost:9001").underlined()
195        );
196    }
197    println!();
198    println!("Update your .env:");
199    println!(
200        "  {}",
201        style("DATABASE_URL=postgres://ferro:ferro_secret@localhost:5432/ferro_db").dim()
202    );
203    if has_mailpit {
204        println!("  {}", style("MAIL_HOST=localhost").dim());
205        println!("  {}", style("MAIL_PORT=1025").dim());
206    }
207    if has_minio {
208        println!("  {}", style("S3_ENDPOINT=http://localhost:9000").dim());
209        println!("  {}", style("S3_ACCESS_KEY=minioadmin").dim());
210        println!("  {}", style("S3_SECRET_KEY=minioadmin").dim());
211    }
212    println!();
213    println!(
214        "{}",
215        style(format!("Network: {project_name}_network")).dim()
216    );
217    println!();
218}