agpm_cli/installer/
selective.rs

1//! Selective installation support for updated resources.
2//!
3//! This module provides targeted installation of only resources that have been
4//! updated, rather than reinstalling all resources. It's designed for efficient
5//! update operations where only a subset of dependencies have changed.
6
7use anyhow::Result;
8use futures::stream::{self, StreamExt};
9use std::collections::HashSet;
10use std::sync::Arc;
11use tokio::sync::Mutex;
12
13use crate::core::ResourceIterator;
14use crate::lockfile::LockFile;
15use crate::manifest::Manifest;
16use crate::utils::progress::ProgressBar;
17
18use super::{InstallContext, install_resource_for_parallel};
19
20/// Install only specific updated resources in parallel (selective installation).
21///
22/// This function provides targeted installation of only the resources that have
23/// been updated, rather than reinstalling all resources. It's designed for
24/// efficient update operations where only a subset of dependencies have changed.
25/// The function uses the same parallel processing architecture as full installations
26/// but operates on a filtered set of resources.
27///
28/// # Arguments
29///
30/// * `updates` - Vector of tuples containing (name, `old_version`, `new_version`) for each updated resource
31/// * `lockfile` - Lockfile containing all available resources (updated resources must exist here)
32/// * `manifest` - Project manifest providing configuration and target directories
33/// * `project_dir` - Root directory where resources will be installed
34/// * `cache` - Cache instance for Git repository and worktree management
35/// * `pb` - Optional progress bar for user feedback during installation
36/// * `_quiet` - Quiet mode flag (currently unused, maintained for API compatibility)
37///
38/// # Update Tuple Format
39///
40/// Each update tuple contains:
41/// - `name`: Resource name as defined in the lockfile
42/// - `old_version`: Previous version (used for logging and user feedback)
43/// - `new_version`: New version that will be installed
44///
45/// # Selective Processing
46///
47/// The function implements selective resource processing:
48/// 1. **Filtering**: Only processes resources listed in the `updates` vector
49/// 2. **Lookup**: Finds corresponding entries in the lockfile for each update
50/// 3. **Validation**: Ensures all specified resources exist before processing
51/// 4. **Installation**: Uses the same parallel architecture as full installations
52///
53/// # Examples
54///
55/// ```rust,no_run
56/// use agpm_cli::installer::{install_updated_resources, InstallContext};
57/// use agpm_cli::lockfile::LockFile;
58/// use agpm_cli::manifest::Manifest;
59/// use agpm_cli::cache::Cache;
60/// use agpm_cli::utils::progress::ProgressBar;
61/// use std::path::Path;
62/// use std::sync::Arc;
63///
64/// # async fn example() -> anyhow::Result<()> {
65/// let lockfile = Arc::new(LockFile::load(Path::new("agpm.lock"))?);
66/// let manifest = Manifest::load(Path::new("agpm.toml"))?;
67/// let cache = Cache::new()?;
68/// let pb = ProgressBar::new(3);
69///
70/// // Define which resources to update
71/// let updates = vec![
72///     ("ai-agent".to_string(), None, "v1.0.0".to_string(), "v1.1.0".to_string()),
73///     ("helper-tool".to_string(), Some("community".to_string()), "v2.0.0".to_string(), "v2.1.0".to_string()),
74///     ("data-processor".to_string(), None, "v1.5.0".to_string(), "v1.6.0".to_string()),
75/// ];
76///
77/// let context = InstallContext::new(Path::new("."), &cache, false, false, Some(&manifest), Some(&lockfile), None, None, None, None, None);
78/// let count = install_updated_resources(
79///     &updates,
80///     &lockfile,
81///     &manifest,
82///     &context,
83///     Some(&pb),
84///     false
85/// ).await?;
86///
87/// println!("Updated {} resources", count);
88/// # Ok(())
89/// # }
90/// ```
91///
92/// # Performance Benefits
93///
94/// Selective installation provides significant performance benefits:
95/// - **Reduced processing**: Only installs resources that have actually changed
96/// - **Faster execution**: Avoids redundant operations on unchanged resources
97/// - **Network efficiency**: Only fetches Git data for repositories with updates
98/// - **Disk efficiency**: Minimizes file system operations and cache usage
99///
100/// # Integration with Update Command
101///
102/// This function is typically used by the `agpm update` command after dependency
103/// resolution determines which resources have new versions available:
104///
105/// ```text
106/// Update Flow:
107/// 1. Resolve dependencies → identify version changes
108/// 2. Update lockfile → record new versions and checksums
109/// 3. Selective installation → install only changed resources
110/// ```
111///
112/// # Returns
113///
114/// Returns the total number of resources that were successfully installed.
115/// This represents the actual number of files that were updated on disk.
116///
117/// # Errors
118///
119/// Returns an error if:
120/// - Any specified resource name is not found in the lockfile
121/// - Git repository access fails for resources being updated
122/// - File system operations fail during installation
123/// - Any individual resource installation encounters an error
124///
125/// The function uses atomic error handling - if any resource fails, the entire
126/// operation fails and detailed error information is provided.
127pub async fn install_updated_resources(
128    updates: &[(String, Option<String>, String, String)], // (name, source, old_version, new_version)
129    lockfile: &Arc<LockFile>,
130    manifest: &Manifest,
131    install_ctx: &InstallContext<'_>,
132    pb: Option<&ProgressBar>,
133    _quiet: bool,
134) -> Result<usize> {
135    let project_dir = install_ctx.project_dir;
136    let cache = install_ctx.cache;
137    if updates.is_empty() {
138        return Ok(0);
139    }
140
141    let total = updates.len();
142
143    // Collect all entries to install
144    let mut entries_to_install = Vec::new();
145    for (name, source, _, _) in updates {
146        if let Some((resource_type, entry)) =
147            ResourceIterator::find_resource_by_name_and_source(lockfile, name, source.as_deref())
148        {
149            // Get artifact configuration path
150            let tool = entry.tool.as_deref().unwrap_or("claude-code");
151            let artifact_path = manifest
152                .get_artifact_resource_path(tool, resource_type)
153                .expect("Resource type should be supported by configured tools");
154            let target_dir = artifact_path.display().to_string();
155            entries_to_install.push((entry.clone(), target_dir));
156        }
157    }
158
159    if entries_to_install.is_empty() {
160        return Ok(0);
161    }
162
163    // Pre-warm the cache by creating all needed worktrees upfront
164    if let Some(pb) = pb {
165        pb.set_message("Preparing resources...");
166    }
167
168    // Collect unique (source, url, sha) triples to pre-create worktrees
169    let mut unique_worktrees = HashSet::new();
170    for (entry, _) in &entries_to_install {
171        if let Some(source_name) = &entry.source
172            && let Some(url) = &entry.url
173        {
174            // Only pre-warm if we have a valid SHA
175            if let Some(sha) = entry.resolved_commit.as_ref().filter(|commit| {
176                commit.len() == 40 && commit.chars().all(|c| c.is_ascii_hexdigit())
177            }) {
178                unique_worktrees.insert((source_name.clone(), url.clone(), sha.clone()));
179            }
180        }
181    }
182
183    // Pre-create all worktrees in parallel
184    if !unique_worktrees.is_empty() {
185        use futures::future;
186        let worktree_futures: Vec<_> = unique_worktrees
187            .into_iter()
188            .map(|(source, url, sha)| {
189                async move {
190                    cache
191                        .get_or_create_worktree_for_sha(
192                            &source,
193                            &url,
194                            &sha,
195                            Some("update-pre-warm"),
196                        )
197                        .await
198                        .ok(); // Ignore errors during pre-warming
199                }
200            })
201            .collect();
202
203        // Execute all worktree creations in parallel
204        future::join_all(worktree_futures).await;
205    }
206
207    // Create thread-safe progress tracking
208    let installed_count = Arc::new(Mutex::new(0));
209    let pb = pb.map(Arc::new);
210    let cache = Arc::new(cache);
211
212    // Set initial progress
213    if let Some(ref pb) = pb {
214        pb.set_message(format!("Installing 0/{total} resources"));
215    }
216
217    // Use concurrent stream processing for parallel installation
218    let results: Vec<Result<(), anyhow::Error>> = stream::iter(entries_to_install)
219        .map(|(entry, resource_dir)| {
220            let project_dir = project_dir.to_path_buf();
221            let installed_count = Arc::clone(&installed_count);
222            let pb = pb.clone();
223            let cache = Arc::clone(&cache);
224            let lockfile = Arc::clone(lockfile);
225
226            async move {
227                // Install the resource
228                let context = InstallContext::new(
229                    &project_dir,
230                    cache.as_ref(),
231                    false,
232                    false, // verbose - will be threaded through from CLI
233                    Some(manifest),
234                    Some(&lockfile),
235                    None, // old_lockfile - not available in parallel context
236                    install_ctx.project_patches,
237                    install_ctx.private_patches,
238                    install_ctx.gitignore_lock,
239                    install_ctx.max_content_file_size,
240                );
241                install_resource_for_parallel(&entry, &resource_dir, &context).await?;
242
243                // Update progress
244                let mut count = installed_count.lock().await;
245                *count += 1;
246
247                if let Some(pb) = pb {
248                    pb.set_message(format!("Installing {}/{} resources", *count, total));
249                    pb.inc(1);
250                }
251
252                Ok::<(), anyhow::Error>(())
253            }
254        })
255        .buffered(usize::MAX) // Allow unlimited task concurrency while preserving input order for deterministic checksums
256        .collect()
257        .await;
258
259    // Check all results for errors
260    for result in results {
261        result?;
262    }
263
264    let final_count = *installed_count.lock().await;
265    Ok(final_count)
266}