agpm_cli/installer/
context.rs

1//! Installation context and helper utilities.
2//!
3//! This module provides the [`InstallContext`] type and its builder for managing
4//! installation parameters throughout the AGPM installation pipeline.
5//!
6//! # Cross-Process Safety
7//!
8//! Cross-process coordination is handled at the command level via `ProjectLock`.
9//! This context no longer carries mutex fields.
10//!
11//! # Examples
12//!
13//! Basic usage with the builder pattern:
14//!
15//! ```rust,no_run
16//! use agpm_cli::installer::InstallContext;
17//! use agpm_cli::cache::Cache;
18//! use std::path::Path;
19//!
20//! # async fn example() -> anyhow::Result<()> {
21//! let project_dir = Path::new(".");
22//! let cache = Cache::new()?;
23//!
24//! // Create a basic context
25//! let context = InstallContext::builder(&project_dir, &cache)
26//!     .force_refresh(true)
27//!     .verbose(false)
28//!     .build();
29//!
30//! // With manifest and lockfile
31//! # use agpm_cli::manifest::Manifest;
32//! # use agpm_cli::lockfile::LockFile;
33//! # use std::sync::Arc;
34//! # let manifest = Manifest::default();
35//! # let lockfile = Arc::new(LockFile::default());
36//! let context = InstallContext::builder(&project_dir, &cache)
37//!     .manifest(&manifest)
38//!     .lockfile(&lockfile)
39//!     .force_refresh(false)
40//!     .build();
41//! # Ok(())
42//! # }
43//! ```
44
45use std::path::Path;
46use std::sync::Arc;
47
48use crate::cache::Cache;
49use crate::lockfile::LockFile;
50use crate::manifest::Manifest;
51
52/// Installation context containing common parameters for resource installation.
53///
54/// This struct bundles frequently-used installation parameters to reduce
55/// function parameter counts and improve code readability. It's used throughout
56/// the installation pipeline to pass configuration and context information.
57///
58/// # Fields
59///
60/// * `project_dir` - Root directory of the project where resources will be installed
61/// * `cache` - Cache instance for managing Git repositories and worktrees
62/// * `force_refresh` - Whether to force refresh of cached worktrees
63/// * `manifest` - Optional reference to the project manifest for template context
64/// * `lockfile` - Optional reference to the lockfile for template context
65/// * `old_lockfile` - Optional reference to the previous lockfile for early-exit optimization
66/// * `project_patches` - Optional project-level patches from agpm.toml
67/// * `private_patches` - Optional user-level patches from agpm.private.toml
68pub struct InstallContext<'a> {
69    pub project_dir: &'a Path,
70    pub cache: &'a Cache,
71    pub force_refresh: bool,
72    pub verbose: bool,
73    pub manifest: Option<&'a Manifest>,
74    pub lockfile: Option<&'a Arc<LockFile>>,
75    pub old_lockfile: Option<&'a LockFile>,
76    pub project_patches: Option<&'a crate::manifest::ManifestPatches>,
77    pub private_patches: Option<&'a crate::manifest::ManifestPatches>,
78    pub max_content_file_size: Option<u64>,
79    /// Shared template context builder for all resources
80    pub template_context_builder: Arc<crate::templating::TemplateContextBuilder>,
81    /// Trust lockfile checksums without recomputing (ultra-fast path optimization).
82    ///
83    /// When enabled and all inputs match the old lockfile entry, skip file I/O and
84    /// return the stored checksum. Safe for immutable dependencies (tags/SHAs).
85    ///
86    /// See module-level docs in [`crate::cli::install`] for optimization tier details.
87    pub trust_lockfile_checksums: bool,
88    /// Token count warning threshold.
89    ///
90    /// When set, resources exceeding this threshold will emit a warning during installation.
91    pub token_warning_threshold: Option<u64>,
92}
93
94/// Builder for creating InstallContext instances with a fluent API.
95pub struct InstallContextBuilder<'a> {
96    // Required parameters
97    project_dir: &'a Path,
98    cache: &'a Cache,
99
100    // Optional with sensible defaults
101    force_refresh: bool,
102    verbose: bool,
103    trust_lockfile_checksums: bool,
104
105    // Truly optional parameters
106    manifest: Option<&'a Manifest>,
107    lockfile: Option<&'a Arc<LockFile>>,
108    old_lockfile: Option<&'a LockFile>,
109    project_patches: Option<&'a crate::manifest::ManifestPatches>,
110    private_patches: Option<&'a crate::manifest::ManifestPatches>,
111    max_content_file_size: Option<u64>,
112    token_warning_threshold: Option<u64>,
113}
114
115impl<'a> InstallContextBuilder<'a> {
116    /// Create a new builder with required parameters.
117    pub fn new(project_dir: &'a Path, cache: &'a Cache) -> Self {
118        Self {
119            project_dir,
120            cache,
121            force_refresh: false,
122            verbose: false,
123            trust_lockfile_checksums: false,
124            manifest: None,
125            lockfile: None,
126            old_lockfile: None,
127            project_patches: None,
128            private_patches: None,
129            max_content_file_size: None,
130            token_warning_threshold: None,
131        }
132    }
133
134    /// Set whether to force refresh of cached worktrees.
135    pub fn force_refresh(mut self, value: bool) -> Self {
136        self.force_refresh = value;
137        self
138    }
139
140    /// Set verbose output.
141    pub fn verbose(mut self, value: bool) -> Self {
142        self.verbose = value;
143        self
144    }
145
146    /// Trust lockfile checksums without recomputing (fast path optimization).
147    ///
148    /// When enabled, if a file exists and all inputs match the old lockfile,
149    /// we return the stored checksum without reading/hashing the file.
150    pub fn trust_lockfile_checksums(mut self, value: bool) -> Self {
151        self.trust_lockfile_checksums = value;
152        self
153    }
154
155    /// Set the project manifest for template context.
156    pub fn manifest(mut self, manifest: &'a Manifest) -> Self {
157        self.manifest = Some(manifest);
158        self
159    }
160
161    /// Set the lockfile for template context.
162    pub fn lockfile(mut self, lockfile: &'a Arc<LockFile>) -> Self {
163        self.lockfile = Some(lockfile);
164        self
165    }
166
167    /// Set the previous lockfile for early-exit optimization.
168    pub fn old_lockfile(mut self, old_lockfile: &'a LockFile) -> Self {
169        self.old_lockfile = Some(old_lockfile);
170        self
171    }
172
173    /// Set project-level patches from agpm.toml.
174    pub fn project_patches(mut self, patches: &'a crate::manifest::ManifestPatches) -> Self {
175        self.project_patches = Some(patches);
176        self
177    }
178
179    /// Set user-level patches from agpm.private.toml.
180    pub fn private_patches(mut self, patches: &'a crate::manifest::ManifestPatches) -> Self {
181        self.private_patches = Some(patches);
182        self
183    }
184
185    /// Set maximum content file size for embedding.
186    pub fn max_content_file_size(mut self, size: u64) -> Self {
187        self.max_content_file_size = Some(size);
188        self
189    }
190
191    /// Set token count warning threshold.
192    pub fn token_warning_threshold(mut self, threshold: u64) -> Self {
193        self.token_warning_threshold = Some(threshold);
194        self
195    }
196
197    /// Set commonly used options in a single call.
198    ///
199    /// This method groups frequently used options to reduce the number of
200    /// builder method calls in common installation scenarios.
201    ///
202    /// # Arguments
203    ///
204    /// * `force_refresh` - Whether to force refresh cached worktrees
205    /// * `verbose` - Whether to enable verbose output
206    /// * `manifest` - Optional project manifest
207    /// * `lockfile` - Optional lockfile for template context
208    pub fn with_common_options(
209        mut self,
210        force_refresh: bool,
211        verbose: bool,
212        manifest: Option<&'a Manifest>,
213        lockfile: Option<&'a Arc<LockFile>>,
214    ) -> Self {
215        self.force_refresh = force_refresh;
216        self.verbose = verbose;
217        self.manifest = manifest;
218        self.lockfile = lockfile;
219        self
220    }
221
222    /// Build the InstallContext with the configured parameters.
223    #[must_use] // The context is needed for installation, ignoring it defeats the purpose
224    pub fn build(self) -> InstallContext<'a> {
225        // Create shared template context builder
226        // Use lockfile if available, otherwise create with empty lockfile
227        let (lockfile_for_builder, project_config) = if let Some(lf) = self.lockfile {
228            (lf.clone(), self.manifest.and_then(|m| m.project.clone()))
229        } else {
230            // No lockfile - create an empty one for the builder
231            (Arc::new(LockFile::default()), None)
232        };
233
234        // Clone cache and wrap in Arc for TemplateContextBuilder
235        // The clone is necessary because we have &Cache but need Arc<Cache>
236        // Cache cloning is relatively cheap (Arc'd internals) and only happens once per installation
237        let template_context_builder = Arc::new(crate::templating::TemplateContextBuilder::new(
238            lockfile_for_builder,
239            project_config,
240            Arc::new(self.cache.clone()),
241            self.project_dir.to_path_buf(),
242        ));
243
244        InstallContext {
245            project_dir: self.project_dir,
246            cache: self.cache,
247            force_refresh: self.force_refresh,
248            verbose: self.verbose,
249            manifest: self.manifest,
250            lockfile: self.lockfile,
251            old_lockfile: self.old_lockfile,
252            project_patches: self.project_patches,
253            private_patches: self.private_patches,
254            max_content_file_size: self.max_content_file_size,
255            template_context_builder,
256            trust_lockfile_checksums: self.trust_lockfile_checksums,
257            token_warning_threshold: self.token_warning_threshold,
258        }
259    }
260}
261
262impl<'a> InstallContext<'a> {
263    /// Create a new builder for InstallContext.
264    pub fn builder(project_dir: &'a Path, cache: &'a Cache) -> InstallContextBuilder<'a> {
265        InstallContextBuilder::new(project_dir, cache)
266    }
267
268    /// Create an InstallContext with common options for parallel installation.
269    ///
270    /// This helper function reduces code duplication by handling the common pattern
271    /// of setting up InstallContext with frequently used options.
272    ///
273    /// # Arguments
274    ///
275    /// * `project_dir` - Root directory of the project
276    /// * `cache` - Cache instance for managing Git repositories
277    /// * `manifest` - Optional project manifest
278    /// * `lockfile` - Lockfile for template context
279    /// * `force_refresh` - Whether to force refresh cached worktrees
280    /// * `verbose` - Whether to enable verbose output
281    /// * `old_lockfile` - Optional previous lockfile for early-exit optimization
282    pub fn with_common_options(
283        project_dir: &'a Path,
284        cache: &'a Cache,
285        manifest: Option<&'a Manifest>,
286        lockfile: Option<&'a Arc<LockFile>>,
287        force_refresh: bool,
288        verbose: bool,
289        old_lockfile: Option<&'a LockFile>,
290    ) -> Self {
291        Self::with_common_options_and_trust(
292            project_dir,
293            cache,
294            manifest,
295            lockfile,
296            force_refresh,
297            verbose,
298            old_lockfile,
299            false, // trust_lockfile_checksums defaults to false
300            None,  // token_warning_threshold defaults to None
301        )
302    }
303
304    /// Create an InstallContext with common options including trust flag.
305    ///
306    /// This is the full version that allows specifying `trust_lockfile_checksums`
307    /// and `token_warning_threshold`.
308    #[allow(clippy::too_many_arguments)]
309    pub fn with_common_options_and_trust(
310        project_dir: &'a Path,
311        cache: &'a Cache,
312        manifest: Option<&'a Manifest>,
313        lockfile: Option<&'a Arc<LockFile>>,
314        force_refresh: bool,
315        verbose: bool,
316        old_lockfile: Option<&'a LockFile>,
317        trust_lockfile_checksums: bool,
318        token_warning_threshold: Option<u64>,
319    ) -> Self {
320        let mut builder = Self::builder(project_dir, cache)
321            .force_refresh(force_refresh)
322            .verbose(verbose)
323            .trust_lockfile_checksums(trust_lockfile_checksums);
324
325        // Add optional fields only if present
326        if let Some(m) = manifest {
327            builder = builder.manifest(m);
328            // Add patches from manifest if available
329            if !m.project_patches.is_empty() {
330                builder = builder.project_patches(&m.project_patches);
331            }
332            if !m.private_patches.is_empty() {
333                builder = builder.private_patches(&m.private_patches);
334            }
335        }
336
337        if let Some(lf) = lockfile {
338            builder = builder.lockfile(lf);
339        }
340
341        if let Some(old_lf) = old_lockfile {
342            builder = builder.old_lockfile(old_lf);
343        }
344
345        if let Some(threshold) = token_warning_threshold {
346            builder = builder.token_warning_threshold(threshold);
347        }
348
349        builder.build()
350    }
351}