1use clap::{Args, Subcommand};
65use ggen_utils::error::Result;
66#[cfg_attr(test, mockall::automock)]
69pub trait ShellInitializer {
70 fn init_shell(&self, shell: &str, config: Option<String>, force: bool) -> Result<InitResult>;
71}
72
73#[cfg_attr(test, mockall::automock)]
74pub trait ProjectInitializer {
75 fn init_project(
76 &self, name: Option<String>, template: Option<String>, here: bool,
77 ) -> Result<InitResult>;
78}
79
80#[cfg_attr(test, mockall::automock)]
81pub trait DevInitializer {
82 fn init_dev(&self, all: bool) -> Result<InitResult>;
83}
84
85#[derive(Debug, Clone)]
86pub struct InitResult {
87 pub stdout: String,
88 pub stderr: String,
89 pub success: bool,
90}
91
92#[derive(Args, Debug)]
93pub struct InitArgs {
94 #[command(subcommand)]
95 pub action: InitAction,
96}
97
98#[derive(Subcommand, Debug)]
99pub enum InitAction {
100 Shell(ShellInitArgs),
102
103 Project(ProjectInitArgs),
105
106 Dev(DevInitArgs),
108}
109
110#[derive(Args, Debug)]
111pub struct ShellInitArgs {
112 #[arg(long, help = "Shell type: bash, zsh, fish, or powershell")]
114 pub shell: String,
115
116 #[arg(
118 long,
119 help = "Path to shell configuration file (auto-detected if not provided)"
120 )]
121 pub config: Option<String>,
122
123 #[arg(long, help = "Force re-initialization even if already configured")]
125 pub force: bool,
126}
127
128#[derive(Args, Debug)]
129pub struct ProjectInitArgs {
130 #[arg(long)]
132 pub name: Option<String>,
133
134 #[arg(long)]
136 pub template: Option<String>,
137
138 #[arg(long)]
140 pub here: bool,
141}
142
143#[derive(Args, Debug)]
144pub struct DevInitArgs {
145 #[arg(long)]
147 pub tool: Option<String>,
148
149 #[arg(long)]
151 pub config: Option<String>,
152}
153
154pub async fn run(args: &InitArgs) -> Result<()> {
155 let shell_init = CargoMakeShellInitializer;
156 let project_init = CargoMakeProjectInitializer;
157 let dev_init = CargoMakeDevInitializer;
158
159 run_with_deps(args, &shell_init, &project_init, &dev_init).await
160}
161
162pub async fn run_with_deps(
163 args: &InitArgs, shell_init: &dyn ShellInitializer, project_init: &dyn ProjectInitializer,
164 dev_init: &dyn DevInitializer,
165) -> Result<()> {
166 match &args.action {
167 InitAction::Shell(shell_args) => init_shell_with_deps(shell_args, shell_init).await,
168 InitAction::Project(project_args) => {
169 init_project_with_deps(project_args, project_init).await
170 }
171 InitAction::Dev(dev_args) => init_dev_with_deps(dev_args, dev_init).await,
172 }
173}
174
175async fn init_shell_with_deps(
176 args: &ShellInitArgs, shell_init: &dyn ShellInitializer,
177) -> Result<()> {
178 println!("Initializing shell integration for {}", args.shell);
179
180 let result = shell_init.init_shell(&args.shell, args.config.clone(), args.force)?;
181
182 if !result.success {
183 return Err(ggen_utils::error::Error::new_fmt(format_args!(
184 "Shell initialization failed: {}",
185 result.stderr
186 )));
187 }
188
189 println!("✅ Shell integration initialized successfully");
190 println!("{}", result.stdout);
191 Ok(())
192}
193
194#[allow(dead_code)]
195async fn init_shell(args: &ShellInitArgs) -> Result<()> {
196 let shell_init = CargoMakeShellInitializer;
197 init_shell_with_deps(args, &shell_init).await
198}
199
200async fn init_project_with_deps(
201 args: &ProjectInitArgs, project_init: &dyn ProjectInitializer,
202) -> Result<()> {
203 println!("Initializing project");
204
205 let result = project_init.init_project(args.name.clone(), args.template.clone(), args.here)?;
206
207 if !result.success {
208 return Err(ggen_utils::error::Error::new_fmt(format_args!(
209 "Project initialization failed: {}",
210 result.stderr
211 )));
212 }
213
214 println!("✅ Project initialized successfully");
215 println!("{}", result.stdout);
216 Ok(())
217}
218
219#[allow(dead_code)]
220async fn init_project(args: &ProjectInitArgs) -> Result<()> {
221 let project_init = CargoMakeProjectInitializer;
222 init_project_with_deps(args, &project_init).await
223}
224
225async fn init_dev_with_deps(_args: &DevInitArgs, dev_init: &dyn DevInitializer) -> Result<()> {
226 println!("Initializing development environment");
227
228 let result = dev_init.init_dev(false)?;
229
230 if !result.success {
231 return Err(ggen_utils::error::Error::new_fmt(format_args!(
232 "Development environment initialization failed: {}",
233 result.stderr
234 )));
235 }
236
237 println!("✅ Development environment initialized successfully");
238 println!("{}", result.stdout);
239 Ok(())
240}
241
242#[allow(dead_code)]
243async fn init_dev(args: &DevInitArgs) -> Result<()> {
244 let dev_init = CargoMakeDevInitializer;
245 init_dev_with_deps(args, &dev_init).await
246}
247
248pub struct CargoMakeShellInitializer;
250
251impl ShellInitializer for CargoMakeShellInitializer {
252 fn init_shell(&self, shell: &str, config: Option<String>, force: bool) -> Result<InitResult> {
253 let mut cmd = std::process::Command::new("cargo");
254 cmd.args(["make", "shell-init"]);
255
256 cmd.arg("--shell").arg(shell);
257
258 if let Some(config) = config {
259 cmd.arg("--config").arg(config);
260 }
261
262 if force {
263 cmd.arg("--force");
264 }
265
266 let output = cmd.output()?;
267 Ok(InitResult {
268 stdout: String::from_utf8_lossy(&output.stdout).to_string(),
269 stderr: String::from_utf8_lossy(&output.stderr).to_string(),
270 success: output.status.success(),
271 })
272 }
273}
274
275pub struct CargoMakeProjectInitializer;
276
277impl ProjectInitializer for CargoMakeProjectInitializer {
278 fn init_project(
279 &self, name: Option<String>, template: Option<String>, here: bool,
280 ) -> Result<InitResult> {
281 let mut cmd = std::process::Command::new("cargo");
282 cmd.args(["make", "project-init"]);
283
284 if let Some(name) = name {
285 cmd.arg("--name").arg(name);
286 }
287
288 if let Some(template) = template {
289 cmd.arg("--template").arg(template);
290 }
291
292 if here {
293 cmd.arg("--here");
294 }
295
296 let output = cmd.output()?;
297 Ok(InitResult {
298 stdout: String::from_utf8_lossy(&output.stdout).to_string(),
299 stderr: String::from_utf8_lossy(&output.stderr).to_string(),
300 success: output.status.success(),
301 })
302 }
303}
304
305pub struct CargoMakeDevInitializer;
306
307impl DevInitializer for CargoMakeDevInitializer {
308 fn init_dev(&self, all: bool) -> Result<InitResult> {
309 let mut cmd = std::process::Command::new("cargo");
310 cmd.args(["make", "dev-init"]);
311
312 if all {
313 cmd.arg("--all");
314 }
315
316 let output = cmd.output()?;
317 Ok(InitResult {
318 stdout: String::from_utf8_lossy(&output.stdout).to_string(),
319 stderr: String::from_utf8_lossy(&output.stderr).to_string(),
320 success: output.status.success(),
321 })
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328 use mockall::predicate::*;
329
330 #[tokio::test]
331 async fn test_init_shell_calls_initializer() {
332 let mut mock = MockShellInitializer::new();
333 mock.expect_init_shell()
334 .with(eq("zsh"), eq(None::<String>), eq(false))
335 .times(1)
336 .returning(|_, _, _| {
337 Ok(InitResult {
338 stdout: "Shell initialized".to_string(),
339 stderr: "".to_string(),
340 success: true,
341 })
342 });
343
344 let args = ShellInitArgs {
345 shell: "zsh".to_string(),
346 config: None,
347 force: false,
348 };
349 let result = init_shell_with_deps(&args, &mock).await;
350 assert!(result.is_ok());
351 }
352
353 #[tokio::test]
354 async fn test_init_shell_with_config() {
355 let mut mock = MockShellInitializer::new();
356 mock.expect_init_shell()
357 .with(eq("bash"), eq(Some("/tmp/bashrc".to_string())), eq(true))
358 .times(1)
359 .returning(|_, _, _| {
360 Ok(InitResult {
361 stdout: "Shell initialized".to_string(),
362 stderr: "".to_string(),
363 success: true,
364 })
365 });
366
367 let args = ShellInitArgs {
368 shell: "bash".to_string(),
369 config: Some("/tmp/bashrc".to_string()),
370 force: true,
371 };
372 let result = init_shell_with_deps(&args, &mock).await;
373 assert!(result.is_ok());
374 }
375
376 #[tokio::test]
377 async fn test_init_project_calls_initializer() {
378 let mut mock = MockProjectInitializer::new();
379 mock.expect_init_project()
380 .with(
381 eq(Some("my-project".to_string())),
382 eq(Some("rust-cli".to_string())),
383 eq(false),
384 )
385 .times(1)
386 .returning(|_, _, _| {
387 Ok(InitResult {
388 stdout: "Project initialized".to_string(),
389 stderr: "".to_string(),
390 success: true,
391 })
392 });
393
394 let args = ProjectInitArgs {
395 name: Some("my-project".to_string()),
396 template: Some("rust-cli".to_string()),
397 here: false,
398 };
399 let result = init_project_with_deps(&args, &mock).await;
400 assert!(result.is_ok());
401 }
402
403 #[tokio::test]
404 async fn test_init_dev_calls_initializer() {
405 let mut mock = MockDevInitializer::new();
406 mock.expect_init_dev()
407 .with(eq(false))
408 .times(1)
409 .returning(|_| {
410 Ok(InitResult {
411 stdout: "Dev environment initialized".to_string(),
412 stderr: "".to_string(),
413 success: true,
414 })
415 });
416
417 let args = DevInitArgs {
418 tool: None,
419 config: None,
420 };
421 let result = init_dev_with_deps(&args, &mock).await;
422 assert!(result.is_ok());
423 }
424
425 #[tokio::test]
426 async fn test_run_with_deps_dispatches_correctly() {
427 let mut mock_shell_init = MockShellInitializer::new();
428 mock_shell_init
429 .expect_init_shell()
430 .with(eq("fish"), eq(None::<String>), eq(false))
431 .times(1)
432 .returning(|_, _, _| {
433 Ok(InitResult {
434 stdout: "Shell initialized".to_string(),
435 stderr: "".to_string(),
436 success: true,
437 })
438 });
439
440 let mock_project_init = MockProjectInitializer::new();
441 let mock_dev_init = MockDevInitializer::new();
442
443 let args = InitArgs {
444 action: InitAction::Shell(ShellInitArgs {
445 shell: "fish".to_string(),
446 config: None,
447 force: false,
448 }),
449 };
450
451 let result =
452 run_with_deps(&args, &mock_shell_init, &mock_project_init, &mock_dev_init).await;
453 assert!(result.is_ok());
454 }
455
456 #[test]
457 fn test_shell_init_args_defaults() {
458 let args = ShellInitArgs {
459 shell: "zsh".to_string(),
460 config: None,
461 force: false,
462 };
463 assert_eq!(args.shell, "zsh");
464 assert!(args.config.is_none());
465 assert!(!args.force);
466 }
467
468 #[test]
469 fn test_project_init_args_defaults() {
470 let args = ProjectInitArgs {
471 name: None,
472 template: None,
473 here: false,
474 };
475 assert!(args.name.is_none());
476 assert!(args.template.is_none());
477 assert!(!args.here);
478 }
479
480 #[test]
481 fn test_dev_init_args_defaults() {
482 let args = DevInitArgs {
483 tool: None,
484 config: None,
485 };
486 assert!(args.tool.is_none());
487 assert!(args.config.is_none());
488 }
489}