1use async_trait::async_trait;
11use futures::future::join_all;
12use std::sync::Arc;
13use tokio::sync::Semaphore;
14
15use crate::error::GgenError;
16use crate::progress::{CacheStatus, InstallationPlan, PlanStep, ProgressReporter};
17use ggen_core::marketplace::{
18 cache::{CacheConfig, CachedPack, PackCache},
19 error::Error as MarketplaceError,
20 AsyncRepository, Package, PackageId, PackageVersion,
21};
22
23pub struct PackInstaller {
25 repository: Box<dyn AsyncRepository<PackageIterator = std::vec::IntoIter<Package>>>,
26 cache: PackCache,
27 progress: ProgressReporter,
28 max_concurrent_downloads: usize,
29}
30
31impl PackInstaller {
32 pub async fn new() -> Result<Self, GgenError> {
34 let cache_config = CacheConfig::default();
36 let cache = PackCache::new(cache_config)
37 .map_err(|e| GgenError::FileError(format!("Failed to initialize cache: {}", e)))?;
38
39 let repository = Self::create_default_repository().await?;
42
43 Ok(Self {
44 repository,
45 cache,
46 progress: ProgressReporter::new(),
47 max_concurrent_downloads: 4,
48 })
49 }
50
51 async fn create_default_repository(
53 ) -> Result<Box<dyn AsyncRepository<PackageIterator = std::vec::IntoIter<Package>>>, GgenError>
54 {
55 Ok(Box::new(TestRepository {}))
58 }
59
60 pub async fn install_pack(
62 &self, pack_id: &str, force_reinstall: bool, dry_run: bool,
63 ) -> Result<InstallResult, GgenError> {
64 let progress = self.progress.clone();
65 progress.start_operation(&format!("Installing pack: {}", pack_id));
66
67 let plan = self.create_installation_plan(pack_id).await?;
69
70 if dry_run {
71 progress.complete();
72 return Ok(InstallResult::DryRun(plan));
73 }
74
75 self.validate_installation_plan(&plan).await?;
77
78 let result = self.execute_installation(&plan, force_reinstall).await;
80
81 progress.complete();
82
83 result
84 }
85
86 async fn create_installation_plan(&self, pack_id: &str) -> Result<InstallationPlan, GgenError> {
88 let progress = self.progress.clone();
89 progress.start_step("Resolving package", 1);
90 progress.set_total_steps(4);
91
92 let package_id = PackageId::new(pack_id)
94 .map_err(|e| GgenError::ValidationError(format!("Invalid pack ID: {}", e)))?;
95
96 let package = self
98 .repository
99 .get_package(&package_id)
100 .await
101 .map_err(|e| self.map_repository_error(e, "get package"))?;
102
103 let latest_version = package.latest_version.clone();
104
105 progress.complete_step("Resolving package");
106 progress.start_step("Resolving dependencies", 2);
107
108 let dependencies = self
110 .resolve_dependencies_tree(&package_id, &latest_version)
111 .await?;
112
113 progress.complete_step("Resolving dependencies");
114 progress.start_step("Checking cache status", 3);
115
116 let cache_status = self
118 .check_cache_status(&package_id, &latest_version, &dependencies)
119 .await?;
120
121 progress.complete_step("Checking cache status");
122 progress.start_step("Calculating installation size", 4);
123
124 let total_size_mb = self.calculate_total_size(&package, &dependencies).await?;
126
127 progress.complete_step("Calculating installation size");
128
129 let steps = self.create_installation_steps(&package, &dependencies, total_size_mb);
131
132 let estimated_duration_seconds = (total_size_mb / 10.0).max(5.0) as u64;
134
135 Ok(InstallationPlan {
136 pack_id: pack_id.to_string(),
137 total_size_mb,
138 estimated_duration_seconds,
139 total_dependencies: dependencies.len(),
140 steps,
141 cache_status,
142 })
143 }
144
145 async fn resolve_dependencies_tree(
147 &self, package_id: &PackageId, _version: &PackageVersion,
148 ) -> Result<Vec<PackageId>, GgenError> {
149 let mut resolved = Vec::new();
150 let mut visited = std::collections::HashSet::new();
151 let mut to_resolve = vec![package_id.clone()];
152
153 while let Some(id) = to_resolve.pop() {
154 if visited.contains(&id) {
155 continue;
156 }
157
158 visited.insert(id.clone());
159 resolved.push(id.clone());
160
161 let dependencies = self.get_resolved_dependencies(&id).await?;
164
165 for dep_id in dependencies {
166 if !visited.contains(&dep_id) {
167 to_resolve.push(dep_id);
168 }
169 }
170 }
171
172 Ok(resolved)
173 }
174
175 async fn check_cache_status(
177 &self, package_id: &PackageId, version: &PackageVersion, dependencies: &[PackageId],
178 ) -> Result<CacheStatus, GgenError> {
179 let mut cached_size_mb = 0.0;
180 let mut cache_hits = 0;
181 let mut total_packages = 1; if let Some(cached) = self.cache.get(package_id, version) {
185 cached_size_mb += cached.size_bytes as f64 / 1_048_576.0;
186 cache_hits += 1;
187 }
188
189 for dep_id in dependencies {
191 if let Ok(dep_version) = self.get_latest_version(dep_id).await {
193 if let Some(cached) = self.cache.get(dep_id, &dep_version) {
194 cached_size_mb += cached.size_bytes as f64 / 1_048_576.0;
195 cache_hits += 1;
196 }
197 }
198 total_packages += 1;
199 }
200
201 Ok(CacheStatus {
202 is_cached: cache_hits == total_packages,
203 cached_size_mb: Some(cached_size_mb),
204 cache_hit: cache_hits > 0,
205 })
206 }
207
208 async fn calculate_total_size(
210 &self, _package: &Package, dependencies: &[PackageId],
211 ) -> Result<f64, GgenError> {
212 let mut total_size = 0.0;
213
214 total_size += 1.0; for dep_id in dependencies {
219 total_size += self.get_package_size(dep_id).await?;
220 }
221
222 Ok(total_size)
223 }
224
225 fn create_installation_steps(
227 &self, _package: &Package, dependencies: &[PackageId], total_size_mb: f64,
228 ) -> Vec<PlanStep> {
229 let mut steps = Vec::new();
230 let step_size_mb = total_size_mb / 6.0; steps.push(PlanStep {
233 step_number: 1,
234 name: "Validate package".to_string(),
235 description: "Checking package integrity and security".to_string(),
236 estimated_duration_ms: 1000,
237 size_mb: 0.1,
238 });
239
240 steps.push(PlanStep {
241 step_number: 2,
242 name: "Download main package".to_string(),
243 description: "Downloading primary package files".to_string(),
244 estimated_duration_ms: (step_size_mb * 1000.0) as u64,
245 size_mb: step_size_mb,
246 });
247
248 if !dependencies.is_empty() {
249 steps.push(PlanStep {
250 step_number: 3,
251 name: "Download dependencies".to_string(),
252 description: format!("Downloading {} dependency packages", dependencies.len()),
253 estimated_duration_ms: (step_size_mb * dependencies.len() as f64 * 1000.0) as u64,
254 size_mb: step_size_mb * dependencies.len() as f64,
255 });
256 }
257
258 steps.push(PlanStep {
259 step_number: 4,
260 name: "Verify signatures".to_string(),
261 description: "Cryptographic signature verification".to_string(),
262 estimated_duration_ms: 2000,
263 size_mb: 0.1,
264 });
265
266 steps.push(PlanStep {
267 step_number: 5,
268 name: "Extract files".to_string(),
269 description: "Extracting package contents to cache".to_string(),
270 estimated_duration_ms: 1500,
271 size_mb: 0.1,
272 });
273
274 steps.push(PlanStep {
275 step_number: 6,
276 name: "Update lockfile".to_string(),
277 description: "Recording installed packages in lockfile".to_string(),
278 estimated_duration_ms: 500,
279 size_mb: 0.1,
280 });
281
282 steps
283 }
284
285 async fn validate_installation_plan(&self, plan: &InstallationPlan) -> Result<(), GgenError> {
287 let required_space_mb = plan.total_size_mb * 2.0;
289 let available_space = self.get_available_disk_space().await?;
290
291 if available_space < required_space_mb {
292 return Err(GgenError::ValidationError(format!(
293 "Insufficient disk space: need {:.1} MB, have {:.1} MB",
294 required_space_mb, available_space
295 )));
296 }
297
298 Ok(())
302 }
303
304 async fn execute_installation(
306 &self, plan: &InstallationPlan, _force_reinstall: bool,
307 ) -> Result<InstallResult, GgenError> {
308 let progress = self.progress.clone();
309 progress.set_total_steps(plan.steps.len());
310
311 let semaphore = Arc::new(Semaphore::new(self.max_concurrent_downloads));
312
313 progress.start_step(&plan.steps[0].name, 1);
315 progress.update_step_progress(0.0, "Validating package...");
316 self.validate_package(&plan.pack_id).await?;
317 progress.complete_step(&plan.steps[0].name);
318
319 progress.start_step(&plan.steps[1].name, 2);
321 progress.update_step_progress(0.0, "Downloading main package...");
322
323 let package_id = PackageId::new(&plan.pack_id)
324 .map_err(|e| GgenError::ValidationError(format!("Invalid pack ID: {}", e)))?;
325
326 let _package = self
327 .download_and_cache_package(&package_id, &plan.steps[1].name)
328 .await?;
329 progress.complete_step(&plan.steps[1].name);
330
331 if !plan.steps[2].name.contains("empty") {
333 progress.start_step(&plan.steps[2].name, 3);
334 progress.update_step_progress(0.0, "Downloading dependencies...");
335
336 let dependency_ids = self.get_resolved_dependencies(&package_id).await?;
337 let total_deps = dependency_ids.len();
338 let download_tasks = dependency_ids.into_iter().map(|dep_id| {
339 let semaphore = semaphore.clone();
340 let progress = progress.clone();
341 async move {
342 #[allow(clippy::unwrap_used)] let _permit = semaphore.acquire().await.unwrap();
344 let dep_name = format!("Dependency: {}", dep_id);
345 progress.update_item_progress(&dep_name, 0, total_deps);
346
347 match self.download_and_cache_package(&dep_id, &dep_name).await {
348 Ok(_dep_package) => {
349 progress.update_item_progress(&dep_name, 1, total_deps);
350 Ok(())
351 }
352 Err(e) => {
353 progress.report_error(
354 &format!("Failed to download {}: {}", dep_id, e),
355 &dep_name,
356 );
357 Err(e)
358 }
359 }
360 }
361 });
362
363 let results = join_all(download_tasks).await;
365
366 for result in results {
367 if let Err(e) = result {
368 return Err(e);
369 }
370 }
371
372 progress.complete_step(&plan.steps[2].name);
373 }
374
375 progress.start_step(&plan.steps[3].name, 4);
377 progress.update_step_progress(0.0, "Verifying signatures...");
378 self.verify_package_signatures(&package_id).await?;
379 progress.complete_step(&plan.steps[3].name);
380
381 progress.start_step(&plan.steps[4].name, 5);
383 progress.update_step_progress(0.0, "Extracting package files...");
384 self.extract_package_files(&package_id).await?;
385 progress.complete_step(&plan.steps[4].name);
386
387 progress.start_step(&plan.steps[5].name, 6);
389 progress.update_step_progress(0.0, "Updating lockfile...");
390 self.update_installation_lockfile(&plan.pack_id).await?;
391 progress.complete_step(&plan.steps[5].name);
392
393 Ok(InstallResult::Success(InstallationResult {
394 pack_id: plan.pack_id.clone(),
395 installed_packages: vec![package_id],
396 cache_status: plan.cache_status.clone(),
397 total_size_mb: plan.total_size_mb,
398 duration_ms: progress.get_state().elapsed().as_millis() as u64,
399 }))
400 }
401
402 async fn download_and_cache_package(
404 &self, package_id: &PackageId, _step_name: &str,
405 ) -> Result<CachedPack, GgenError> {
406 let progress = self.progress.clone();
407 progress.update_step_progress(0.0, &format!("Checking cache for {}", package_id));
408
409 let latest_version = self.get_latest_version(package_id).await.map_err(|e| {
411 GgenError::ValidationError(format!("No version found for {}: {}", package_id, e))
412 })?;
413
414 if let Some(cached) = self.cache.get(package_id, &latest_version) {
415 progress
416 .update_step_progress(100.0, &format!("Using cached version of {}", package_id));
417 return Ok(cached);
418 }
419
420 progress.update_step_progress(0.0, &format!("Downloading {}", package_id));
422
423 let package = self
424 .repository
425 .get_package_version(package_id, &latest_version)
426 .await
427 .map_err(|e| self.map_repository_error(e, "download package"))?;
428
429 let total_size = 1.0 * 1024.0 * 1024.0;
431 let mut downloaded = 0.0;
432 let progress_step = 100.0 / 10.0; for i in 0..10 {
435 tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
436 downloaded += total_size / 10.0;
437 progress.update_data_progress(downloaded as u64, total_size as u64);
438 progress.update_step_progress(
439 (i + 1) as f64 * progress_step,
440 &format!("Downloading {}...", i + 1),
441 );
442 }
443
444 let cached_package = self
446 .cache_package(package_id, &latest_version, package)
447 .await?;
448
449 progress.update_step_progress(100.0, &format!("Cached {}", package_id));
450 Ok(cached_package)
451 }
452
453 async fn get_resolved_dependencies(
455 &self, _package_id: &PackageId,
456 ) -> Result<Vec<PackageId>, GgenError> {
457 Ok(vec![])
459 }
460
461 async fn get_latest_version(
462 &self, _package_id: &PackageId,
463 ) -> Result<PackageVersion, GgenError> {
464 Ok(PackageVersion::new("1.0.0")
465 .map_err(|e| GgenError::ValidationError(format!("Invalid version: {}", e)))?)
466 }
467
468 async fn get_package_size(&self, _package_id: &PackageId) -> Result<f64, GgenError> {
469 Ok(1.0) }
471
472 async fn get_available_disk_space(&self) -> Result<f64, GgenError> {
473 Ok(1024.0)
475 }
476
477 async fn validate_package(&self, _package_id: &str) -> Result<(), GgenError> {
478 Ok(())
480 }
481
482 async fn verify_package_signatures(&self, _package_id: &PackageId) -> Result<(), GgenError> {
483 Ok(())
485 }
486
487 async fn extract_package_files(&self, _package_id: &PackageId) -> Result<(), GgenError> {
488 Ok(())
490 }
491
492 async fn update_installation_lockfile(&self, _pack_id: &str) -> Result<(), GgenError> {
493 Ok(())
495 }
496
497 async fn cache_package(
498 &self, _package_id: &PackageId, _version: &PackageVersion, _package: Package,
499 ) -> Result<CachedPack, GgenError> {
500 Err(GgenError::ValidationError(
502 "Caching logic requires repository backend implementation".to_string(),
503 ))
504 }
505
506 fn map_repository_error(&self, error: MarketplaceError, operation: &str) -> GgenError {
507 match error {
508 MarketplaceError::PackageNotFound { package_id } => GgenError::ValidationError(
509 format!("Package '{}' not found in marketplace", package_id),
510 ),
511 MarketplaceError::IoError(e) => {
512 GgenError::NetworkError(format!("Network/IO error while {}: {}", operation, e))
513 }
514 MarketplaceError::ValidationFailed { reason } => GgenError::ValidationError(format!(
515 "Validation error while {}: {}",
516 operation, reason
517 )),
518 _ => GgenError::FileError(format!("Failed to {}: {}", operation, error)),
519 }
520 }
521}
522
523struct TestRepository {}
525
526#[allow(clippy::unwrap_used)] #[async_trait]
528impl AsyncRepository for TestRepository {
529 type PackageIterator = std::vec::IntoIter<Package>;
530
531 async fn get_package(&self, package_id: &PackageId) -> Result<Package, MarketplaceError> {
532 let version = PackageVersion::new("1.0.0").unwrap();
533 let metadata = ggen_core::marketplace::PackageMetadata::new(
534 package_id.clone(),
535 format!("Pack {}", package_id),
536 "Mock description",
537 "MIT",
538 );
539 Ok(Package {
540 metadata,
541 latest_version: version.clone(),
542 versions: vec![version],
543 releases: indexmap::IndexMap::new(),
544 })
545 }
546
547 async fn get_package_version(
548 &self, package_id: &PackageId, version: &PackageVersion,
549 ) -> Result<Package, MarketplaceError> {
550 let metadata = ggen_core::marketplace::PackageMetadata::new(
551 package_id.clone(),
552 format!("Pack {}", package_id),
553 "Mock description",
554 "MIT",
555 );
556 Ok(Package {
557 metadata,
558 latest_version: version.clone(),
559 versions: vec![version.clone()],
560 releases: indexmap::IndexMap::new(),
561 })
562 }
563
564 async fn all_packages(&self) -> Result<Vec<Package>, MarketplaceError> {
565 let id = PackageId::new("test-pack").unwrap();
566 Ok(vec![self.get_package(&id).await?])
567 }
568
569 async fn list_versions(
570 &self, _id: &PackageId,
571 ) -> Result<Vec<PackageVersion>, MarketplaceError> {
572 Ok(vec![PackageVersion::new("1.0.0").unwrap()])
573 }
574
575 async fn package_exists(&self, _id: &PackageId) -> Result<bool, MarketplaceError> {
576 Ok(true)
577 }
578}
579
580#[derive(Debug, Clone, serde::Serialize)]
582pub enum InstallResult {
583 Success(InstallationResult),
584 DryRun(InstallationPlan),
585}
586
587#[derive(Debug, Clone, serde::Serialize)]
588pub struct InstallationResult {
589 pub pack_id: String,
590 pub installed_packages: Vec<PackageId>,
591 pub cache_status: CacheStatus,
592 pub total_size_mb: f64,
593 pub duration_ms: u64,
594}
595
596impl From<InstallResult> for crate::cmds::pack::InstallOutput {
597 fn from(result: InstallResult) -> Self {
598 match result {
599 InstallResult::Success(success) => crate::cmds::pack::InstallOutput {
600 pack_id: success.pack_id.clone(),
601 pack_name: success.pack_id.clone(),
602 status: "installed".to_string(),
603 message: format!(
604 "Pack installed successfully. Size: {:.1} MB, Duration: {}ms",
605 success.total_size_mb, success.duration_ms
606 ),
607 },
608 InstallResult::DryRun(plan) => crate::cmds::pack::InstallOutput {
609 pack_id: plan.pack_id.clone(),
610 pack_name: plan.pack_id.clone(),
611 status: "dry_run".to_string(),
612 message: format!(
613 "Dry run: Would install {:.1} MB with {} dependencies. Estimated: {}s",
614 plan.total_size_mb, plan.total_dependencies, plan.estimated_duration_seconds
615 ),
616 },
617 }
618 }
619}
620
621#[cfg(test)]
622mod tests {
623 use super::*;
624
625 #[tokio::test]
626 async fn test_pack_installation_plan_creation() {
627 let installer = PackInstaller::new().await.unwrap();
628 let plan = installer
629 .create_installation_plan("test-pack")
630 .await
631 .unwrap();
632
633 assert_eq!(plan.pack_id, "test-pack");
634 assert!(plan.total_size_mb > 0.0);
635 assert!(!plan.steps.is_empty());
636 assert!(plan.estimated_duration_seconds > 0);
637 }
638
639 #[tokio::test]
640 async fn test_dry_run_installation() {
641 let installer = PackInstaller::new().await.unwrap();
642 let result = installer
643 .install_pack("test-pack", false, true)
644 .await
645 .unwrap();
646
647 match result {
648 InstallResult::DryRun(plan) => {
649 assert_eq!(plan.pack_id, "test-pack");
650 }
651 InstallResult::Success(_) => panic!("Expected dry run result"),
652 }
653 }
654}