1use 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
19pub 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 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 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 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 let git_operations = GitOperationsImpl::new();
71
72 let repo_root = if let Some(parent) = self.config_path.parent() {
74 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 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 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 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 let git_operations = GitOperationsImpl::new();
132
133 let repo_root = if let Some(parent) = self.config_path.parent() {
135 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 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 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 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 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 let repo_root = if let Some(parent) = self.config_path.parent() {
195 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 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 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 let config = config_repo
220 .load(&self.config_path)
221 .context("Failed to load configuration")?;
222
223 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 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 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 pub fn show_dependency_status(
281 &self,
282 dependencies: Option<Vec<String>>,
283 detailed: bool,
284 ) -> Result<()> {
285 let error_handler = crate::domain::examples::ErrorHandler::new();
287 let config_repo = TomlConfigurationRepository::new();
289 let status_query = crate::application::status::GetDependencyStatusQuery::new(config_repo);
290
291 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 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 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 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 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 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 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 let status = git_ops.get_status(repo_path)?;
372 if !status.is_clean() {
373 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 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}