ferro_cli/commands/
make_task.rs1use console::style;
4use std::fs;
5use std::path::Path;
6
7use crate::templates;
8
9pub fn run(name: String) {
10 let struct_name = to_pascal_case(&name);
12
13 let struct_name = if struct_name.ends_with("Task") {
15 struct_name
16 } else {
17 format!("{struct_name}Task")
18 };
19
20 let file_name = to_snake_case(&struct_name);
22
23 if !is_valid_identifier(&file_name) {
25 eprintln!(
26 "{} '{}' is not a valid task name",
27 style("Error:").red().bold(),
28 name
29 );
30 std::process::exit(1);
31 }
32
33 let tasks_dir = Path::new("src/tasks");
34 let task_file = tasks_dir.join(format!("{file_name}.rs"));
35 let mod_file = tasks_dir.join("mod.rs");
36 let schedule_file = Path::new("src/schedule.rs");
37
38 if !Path::new("src").exists() {
40 eprintln!(
41 "{} Not in a Ferro project root directory",
42 style("Error:").red().bold()
43 );
44 eprintln!(
45 "{}",
46 style("Make sure you're in a Ferro project directory with a src/ folder.").dim()
47 );
48 std::process::exit(1);
49 }
50
51 if !tasks_dir.exists() {
53 if let Err(e) = fs::create_dir_all(tasks_dir) {
54 eprintln!(
55 "{} Failed to create tasks directory: {}",
56 style("Error:").red().bold(),
57 e
58 );
59 std::process::exit(1);
60 }
61 println!("{} Created src/tasks/", style("✓").green());
62
63 let mod_content = templates::tasks_mod();
65 if let Err(e) = fs::write(&mod_file, mod_content) {
66 eprintln!(
67 "{} Failed to create mod.rs: {}",
68 style("Error:").red().bold(),
69 e
70 );
71 std::process::exit(1);
72 }
73 println!("{} Created src/tasks/mod.rs", style("✓").green());
74 }
75
76 if !schedule_file.exists() {
78 let schedule_content = templates::schedule_rs();
79 if let Err(e) = fs::write(schedule_file, schedule_content) {
80 eprintln!(
81 "{} Failed to create schedule.rs: {}",
82 style("Error:").red().bold(),
83 e
84 );
85 std::process::exit(1);
86 }
87 println!("{} Created src/schedule.rs", style("✓").green());
88 }
89
90 if task_file.exists() {
95 eprintln!(
96 "{} Task '{}' already exists at {}",
97 style("Info:").yellow().bold(),
98 struct_name,
99 task_file.display()
100 );
101 std::process::exit(0);
102 }
103
104 if mod_file.exists() {
106 let mod_content = fs::read_to_string(&mod_file).unwrap_or_default();
107 let mod_decl = format!("mod {file_name};");
108 let pub_mod_decl = format!("pub mod {file_name};");
109 if mod_content.contains(&mod_decl) || mod_content.contains(&pub_mod_decl) {
110 eprintln!(
111 "{} Module '{}' is already declared in src/tasks/mod.rs",
112 style("Info:").yellow().bold(),
113 file_name
114 );
115 std::process::exit(0);
116 }
117 }
118
119 let task_content = templates::task_template(&file_name, &struct_name);
121
122 if let Err(e) = fs::write(&task_file, task_content) {
124 eprintln!(
125 "{} Failed to write task file: {}",
126 style("Error:").red().bold(),
127 e
128 );
129 std::process::exit(1);
130 }
131 println!("{} Created {}", style("✓").green(), task_file.display());
132
133 if let Err(e) = update_mod_file(&mod_file, &file_name, &struct_name) {
135 eprintln!(
136 "{} Failed to update mod.rs: {}",
137 style("Error:").red().bold(),
138 e
139 );
140 std::process::exit(1);
141 }
142 println!("{} Updated src/tasks/mod.rs", style("✓").green());
143
144 println!();
145 println!(
146 "Task {} created successfully!",
147 style(&struct_name).cyan().bold()
148 );
149 println!();
150 println!("Next steps:");
151 println!(
152 " {} Implement your task logic in {}",
153 style("1.").dim(),
154 task_file.display()
155 );
156 println!();
157 println!(
158 " {} Register the task in src/schedule.rs:",
159 style("2.").dim()
160 );
161 println!(
162 " {}",
163 style(format!("use crate::tasks::{file_name};")).cyan()
164 );
165 println!(
166 " {}",
167 style(format!("schedule.task({struct_name}::new());")).cyan()
168 );
169 println!();
170 println!(" {} Run the scheduler:", style("3.").dim());
171 println!(" ferro schedule:work {} Daemon mode", style("#").dim());
172 println!(" ferro schedule:run {} Run once", style("#").dim());
173 println!(" ferro schedule:list {} List tasks", style("#").dim());
174 println!();
175}
176
177fn is_valid_identifier(name: &str) -> bool {
178 if name.is_empty() {
179 return false;
180 }
181
182 let mut chars = name.chars();
183
184 match chars.next() {
186 Some(c) if c.is_alphabetic() || c == '_' => {}
187 _ => return false,
188 }
189
190 chars.all(|c| c.is_alphanumeric() || c == '_')
192}
193
194fn to_snake_case(s: &str) -> String {
195 let mut result = String::new();
196 for (i, c) in s.chars().enumerate() {
197 if c.is_uppercase() {
198 if i > 0 {
199 result.push('_');
200 }
201 result.push(c.to_lowercase().next().unwrap());
202 } else {
203 result.push(c);
204 }
205 }
206 result
207}
208
209fn to_pascal_case(s: &str) -> String {
210 let mut result = String::new();
211 let mut capitalize_next = true;
212
213 for c in s.chars() {
214 if c == '_' || c == '-' || c == ' ' {
215 capitalize_next = true;
216 } else if capitalize_next {
217 result.push(c.to_uppercase().next().unwrap());
218 capitalize_next = false;
219 } else {
220 result.push(c);
221 }
222 }
223 result
224}
225
226fn update_mod_file(mod_file: &Path, file_name: &str, struct_name: &str) -> Result<(), String> {
227 let content =
228 fs::read_to_string(mod_file).map_err(|e| format!("Failed to read mod.rs: {e}"))?;
229
230 let pub_mod_decl = format!("pub mod {file_name};");
231 let pub_use_decl = format!("pub use {file_name}::{struct_name};");
232
233 let lines: Vec<&str> = content.lines().collect();
235
236 let mut last_pub_mod_idx = None;
238 let mut last_pub_use_idx = None;
239
240 for (i, line) in lines.iter().enumerate() {
241 if line.trim().starts_with("pub mod ") {
242 last_pub_mod_idx = Some(i);
243 }
244 if line.trim().starts_with("pub use ") {
245 last_pub_use_idx = Some(i);
246 }
247 }
248
249 let mut new_lines: Vec<String> = Vec::new();
251
252 if let Some(idx) = last_pub_mod_idx {
254 for (i, line) in lines.iter().enumerate() {
255 new_lines.push(line.to_string());
256 if i == idx {
257 new_lines.push(pub_mod_decl.clone());
258 }
259 }
260 } else {
261 let mut content_end = lines.len();
263 while content_end > 0 && lines[content_end - 1].trim().is_empty() {
264 content_end -= 1;
265 }
266
267 for (i, line) in lines.iter().enumerate() {
268 new_lines.push(line.to_string());
269 if i == content_end.saturating_sub(1) || (content_end == 0 && i == 0) {
270 new_lines.push(pub_mod_decl.clone());
271 }
272 }
273
274 if lines.is_empty() {
276 new_lines.push(pub_mod_decl.clone());
277 }
278 }
279
280 if last_pub_use_idx.is_some() {
282 let mut insert_idx = None;
284 for (i, line) in new_lines.iter().enumerate() {
285 if line.trim().starts_with("pub use ") {
286 insert_idx = Some(i);
287 }
288 }
289 if let Some(idx) = insert_idx {
290 new_lines.insert(idx + 1, pub_use_decl);
291 }
292 }
293
294 let new_content = new_lines.join("\n");
295
296 let new_content = if new_content.ends_with('\n') {
298 new_content
299 } else {
300 format!("{new_content}\n")
301 };
302
303 fs::write(mod_file, new_content).map_err(|e| format!("Failed to write mod.rs: {e}"))?;
304
305 Ok(())
306}