Skip to main content

wrale_acdm/interfaces/
cli.rs

1// Copyright (c) 2025 Wrale LTD <contact@wrale.com>
2
3use crate::domain::repositories::{ConfigurationRepository, GitOperations};
4use anyhow::{anyhow, Context, Result};
5use log::{debug, info, warn};
6use std::io::Write;
7use std::path::PathBuf;
8
9use crate::application::dto::{
10    AddDependencyDto, IncludePathsDto, InitConfigDto, UpdateDependenciesDto,
11};
12use crate::application::use_cases::{
13    AddDependencyUseCase, IncludePathsUseCase, InitConfigUseCase, UpdateDependenciesUseCase,
14};
15use crate::infrastructure::configuration::TomlConfigurationRepository;
16use crate::infrastructure::file_system::FileSystemManagerImpl;
17use crate::infrastructure::git::{GitOperationsImpl, GitRepositoryFetcher};
18
19/// Adapter for the CLI interface
20pub struct CliAdapter {
21    config_path: PathBuf,
22}
23
24impl CliAdapter {
25    pub fn new(config_path: PathBuf) -> Self {
26        Self { config_path }
27    }
28
29    /// Initialize a new configuration
30    pub fn init(&self, default_location: Option<String>, force: bool) -> Result<()> {
31        debug!(
32            "Initializing configuration with default location: {:?}, force: {}",
33            default_location, force
34        );
35
36        // Check if config file already exists and force is not enabled
37        if self.config_path.exists() && !force {
38            return Err(anyhow!(
39                "Configuration file already exists at {}. Use --force to overwrite.",
40                self.config_path.display()
41            ));
42        }
43
44        let config_repo = TomlConfigurationRepository::new();
45        let use_case = InitConfigUseCase::new(config_repo);
46
47        use_case
48            .execute(InitConfigDto {
49                config_path: self.config_path.clone(),
50                default_location,
51            })
52            .context("Failed to initialize configuration")
53    }
54
55    /// Add a new dependency
56    pub fn add_dependency(
57        &self,
58        name: String,
59        repository_url: String,
60        revision: String,
61        target_location: String,
62        force: bool,
63    ) -> Result<()> {
64        debug!(
65            "Adding dependency: name={}, url={}, rev={}, target={}",
66            name, repository_url, revision, target_location
67        );
68
69        // Create Git operations and verify clean status
70        let git_operations = GitOperationsImpl::new();
71
72        // Get the repository root - if we have a parent directory, use it, otherwise use the current directory
73        let repo_root = if let Some(parent) = self.config_path.parent() {
74            // If parent is empty, use current directory
75            if parent.as_os_str().is_empty() {
76                std::env::current_dir()
77                    .map_err(|e| anyhow!("Failed to get current directory: {}", e))?
78            } else {
79                parent.to_path_buf()
80            }
81        } else {
82            // No parent means the config is in the current directory
83            std::env::current_dir()
84                .map_err(|e| anyhow!("Failed to get current directory: {}", e))?
85        };
86
87        debug!("Using repository root path: {}", repo_root.display());
88
89        // Skip this check if force is enabled
90        if !force {
91            if let Err(e) = self.ensure_clean_git_status(&git_operations, &repo_root) {
92                warn!("Git repository status is not clean: {}", e);
93                return Err(e);
94            }
95        }
96
97        let config_repo = TomlConfigurationRepository::new();
98        let use_case = AddDependencyUseCase::new(config_repo);
99
100        use_case
101            .execute(
102                &self.config_path,
103                AddDependencyDto {
104                    name: name.clone(),
105                    repository_url,
106                    revision,
107                    repository_type: "git".to_string(),
108                    target_location,
109                },
110            )
111            .context("Failed to add dependency")?;
112
113        info!("Remember to commit your changes manually with 'git add . && git commit -m \"Add dependency {name}\"'");
114
115        Ok(())
116    }
117
118    /// Include paths in a dependency
119    pub fn include_paths(
120        &self,
121        dependency_name: String,
122        paths: Vec<String>,
123        force: bool,
124    ) -> Result<()> {
125        debug!(
126            "Including paths for dependency: {}, paths: {:?}",
127            dependency_name, paths
128        );
129
130        // Create Git operations and verify clean status
131        let git_operations = GitOperationsImpl::new();
132
133        // Get the repository root - if we have a parent directory, use it, otherwise use the current directory
134        let repo_root = if let Some(parent) = self.config_path.parent() {
135            // If parent is empty, use current directory
136            if parent.as_os_str().is_empty() {
137                std::env::current_dir()
138                    .map_err(|e| anyhow!("Failed to get current directory: {}", e))?
139            } else {
140                parent.to_path_buf()
141            }
142        } else {
143            // No parent means the config is in the current directory
144            std::env::current_dir()
145                .map_err(|e| anyhow!("Failed to get current directory: {}", e))?
146        };
147
148        debug!("Using repository root path: {}", repo_root.display());
149
150        // Skip this check if force is enabled
151        if !force {
152            if let Err(e) = self.ensure_clean_git_status(&git_operations, &repo_root) {
153                warn!("Git repository status is not clean: {}", e);
154                return Err(e);
155            }
156        }
157
158        let config_repo = TomlConfigurationRepository::new();
159        let use_case = IncludePathsUseCase::new(config_repo);
160
161        use_case
162            .execute(
163                &self.config_path,
164                IncludePathsDto {
165                    dependency_name: dependency_name.clone(),
166                    paths: paths.clone(),
167                },
168            )
169            .context("Failed to include paths")?;
170
171        info!("Remember to commit your changes manually with 'git add . && git commit -m \"Include paths for {dependency_name}\"'");
172
173        Ok(())
174    }
175
176    /// Update dependencies
177    pub fn update_dependencies(
178        &self,
179        dependencies: Option<Vec<String>>,
180        force: bool,
181    ) -> Result<()> {
182        debug!(
183            "Updating dependencies: {:?}, force: {}",
184            dependencies, force
185        );
186
187        // Create required components
188        let config_repo = TomlConfigurationRepository::new();
189        let repository_fetcher = GitRepositoryFetcher::new();
190        let file_system_manager = FileSystemManagerImpl::new();
191        let git_operations = GitOperationsImpl::new();
192
193        // Get the repository root - if we have a parent directory, use it, otherwise use the current directory
194        let repo_root = if let Some(parent) = self.config_path.parent() {
195            // If parent is empty, use current directory
196            if parent.as_os_str().is_empty() {
197                std::env::current_dir()
198                    .map_err(|e| anyhow!("Failed to get current directory: {}", e))?
199            } else {
200                parent.to_path_buf()
201            }
202        } else {
203            // No parent means the config is in the current directory
204            std::env::current_dir()
205                .map_err(|e| anyhow!("Failed to get current directory: {}", e))?
206        };
207
208        debug!("Using repository root path: {}", repo_root.display());
209
210        // Verify Git status if not in force mode
211        if !force {
212            if let Err(e) = self.ensure_clean_git_status(&git_operations, &repo_root) {
213                warn!("Git repository status is not clean: {}", e);
214                return Err(e);
215            }
216        }
217
218        // Load configuration and determine what will be updated
219        let config = config_repo
220            .load(&self.config_path)
221            .context("Failed to load configuration")?;
222
223        // Get the dependencies to update
224        let dependencies_to_update = if let Some(dep_names) = dependencies.as_ref() {
225            let filtered_deps = config
226                .dependencies
227                .iter()
228                .filter(|d| dep_names.contains(&d.name))
229                .collect::<Vec<_>>();
230
231            if filtered_deps.is_empty() {
232                return Err(anyhow!("No matching dependencies found to update"));
233            }
234
235            filtered_deps
236        } else {
237            if config.dependencies.is_empty() {
238                return Err(anyhow!("No dependencies found to update"));
239            }
240
241            config.dependencies.iter().collect()
242        };
243
244        // Show warning about what mount points will be purged
245        if !force {
246            info!("The following mount points will be purged:");
247            for dep in dependencies_to_update.iter() {
248                info!("  - {}", dep.target_location.display());
249            }
250
251            if !self.prompt_yes_no("Do you want to continue with the update?")? {
252                info!("Update canceled by user");
253                return Ok(());
254            }
255        }
256
257        // Run the update
258        let use_case = UpdateDependenciesUseCase::new(
259            config_repo,
260            repository_fetcher,
261            file_system_manager,
262            git_operations,
263        );
264
265        debug!("Executing update dependencies use case");
266        use_case
267            .execute(UpdateDependenciesDto {
268                config_path: self.config_path.clone(),
269                dependencies: dependencies.clone(),
270                force,
271            })
272            .context("Failed to update dependencies")?;
273
274        info!("Remember to commit your changes manually with 'git add . && git commit -m \"Update dependencies\"'");
275
276        Ok(())
277    }
278
279    /// Show dependency status
280    pub fn show_dependency_status(
281        &self,
282        dependencies: Option<Vec<String>>,
283        detailed: bool,
284    ) -> Result<()> {
285        // Add error handler for better error messages
286        let error_handler = crate::domain::examples::ErrorHandler::new();
287        // Create components
288        let config_repo = TomlConfigurationRepository::new();
289        let status_query = crate::application::status::GetDependencyStatusQuery::new(config_repo);
290
291        // Get statuses
292        let statuses = match status_query.get_all_statuses(&self.config_path) {
293            Ok(s) => s,
294            Err(e) => {
295                eprintln!(
296                    "{}",
297                    error_handler.display_error(&crate::domain::DomainError::ConfigurationError(
298                        e.to_string()
299                    ))
300                );
301                return Err(e);
302            }
303        };
304
305        // Filter by dependencies if specified
306        let filtered_statuses = if let Some(dep_names) = dependencies {
307            statuses
308                .into_iter()
309                .filter(|s| dep_names.contains(&s.name))
310                .collect::<Vec<_>>()
311        } else {
312            statuses
313        };
314
315        if filtered_statuses.is_empty() {
316            println!("No dependencies found");
317            return Ok(());
318        }
319
320        // Display the statuses
321        println!("Dependencies:");
322        for status in filtered_statuses {
323            println!("  - {}: {}", status.name, status.status);
324
325            if detailed {
326                println!("    Repository: {}", status.repository_url);
327                println!("    Revision:   {}", status.revision);
328                println!("    Target:     {}", status.target_location);
329                println!("    Paths:      {}", status.sparse_paths.join(", "));
330
331                // Use auth service to get auth info
332                let auth_service = crate::domain::auth::AuthenticationService::new();
333                if let Some(auth) = auth_service.get_auth_for_repository(&status.repository_url) {
334                    let auth_helper = crate::domain::examples::AuthHelper::new();
335                    let auth_info = auth_helper.format_credentials(&auth);
336                    println!("    Auth:       {}", auth_info);
337                }
338            }
339        }
340
341        Ok(())
342    }
343
344    /// Prompt user for confirmation with yes/no
345    fn prompt_yes_no(&self, message: &str) -> Result<bool> {
346        let mut input = String::new();
347        print!("{} [y/N]: ", message);
348        std::io::stdout().flush()?;
349        std::io::stdin().read_line(&mut input)?;
350
351        Ok(input.trim().to_lowercase() == "y")
352    }
353
354    /// Ensure the Git repository has a clean status and exists
355    fn ensure_clean_git_status(
356        &self,
357        git_ops: &GitOperationsImpl,
358        repo_path: &std::path::Path,
359    ) -> Result<()> {
360        debug!(
361            "Verifying Git repository status for path: {}",
362            repo_path.display()
363        );
364
365        // First check if it's a git repository at all
366        if !git_ops.is_git_repository(repo_path)? {
367            return Err(anyhow!("Directory is not a Git repository. This tool requires operations to be performed within a Git repository."));
368        }
369
370        // Check if it has a clean status
371        let status = git_ops.get_status(repo_path)?;
372        if !status.is_clean() {
373            // Run git status to show the user what's going on
374            let git_status_output = std::process::Command::new("git")
375                .args(["status", "--short"])
376                .current_dir(repo_path)
377                .output();
378
379            let mut error_msg = String::from("Git repository has uncommitted changes. Please commit your changes before proceeding or use the --force flag (NOT RECOMMENDED)");
380
381            // Add git status output if available
382            if let Ok(output) = git_status_output {
383                if output.status.success() {
384                    let status_text = String::from_utf8_lossy(&output.stdout).trim().to_string();
385                    if !status_text.is_empty() {
386                        error_msg.push_str("\n\nGit status:\n");
387                        error_msg.push_str(&status_text);
388                    }
389                }
390            }
391
392            return Err(anyhow!(error_msg));
393        }
394
395        Ok(())
396    }
397}