ferro_cli/commands/
docker_compose.rs1use 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 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 let project_name = get_project_name();
24
25 let compose_path = Path::new("docker-compose.yml");
26
27 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 let (include_mailpit, include_minio) = prompt_for_services(with_mailpit, with_minio);
42
43 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();
58
59 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 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 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 if content.contains("docker-compose.override.yml") {
135 return;
136 }
137
138 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}