agpm_cli/cli/upgrade.rs
1use crate::config::GlobalConfig;
2use crate::upgrade::{SelfUpdater, backup::BackupManager, version_check::VersionChecker};
3use anyhow::{Context, Result, bail};
4use clap::Parser;
5use colored::Colorize;
6use std::env;
7use tracing::debug;
8
9/// Command-line arguments for the AGPM upgrade command.
10///
11/// This structure defines all the options and flags available for upgrading
12/// AGPM to newer versions. The upgrade command provides multiple modes of
13/// operation from simple version checking to full upgrades with rollback support.
14///
15/// # Command Modes
16///
17/// The upgrade command operates in several distinct modes:
18///
19/// ## Update Modes
20/// - **Check Only** (`--check`): Check for updates without installing
21/// - **Status Display** (`--status`): Show current and latest version information
22/// - **Upgrade to Latest**: Default behavior when no version specified
23/// - **Upgrade to Specific Version**: When version argument is provided
24///
25/// ## Safety Modes
26/// - **Normal Upgrade**: Creates backup and upgrades with safety checks
27/// - **Force Upgrade** (`--force`): Bypass version checks and force installation
28/// - **No Backup** (`--no-backup`): Skip backup creation (not recommended)
29/// - **Rollback** (`--rollback`): Restore from previous backup
30///
31/// # Examples
32///
33/// ## Basic Usage
34/// ```bash
35/// # Check for available updates
36/// agpm upgrade --check
37///
38/// # Show version status
39/// agpm upgrade --status
40///
41/// # Upgrade to latest version
42/// agpm upgrade
43/// ```
44///
45/// ## Version-Specific Upgrades
46/// ```bash
47/// # Upgrade to specific version
48/// agpm upgrade 0.4.0
49/// agpm upgrade v0.4.0
50///
51/// # Force upgrade even if already on target version
52/// agpm upgrade 0.4.0 --force
53/// ```
54///
55/// ## Safety and Recovery
56/// ```bash
57/// # Upgrade without creating backup (risky)
58/// agpm upgrade --no-backup
59///
60/// # Rollback to previous version
61/// agpm upgrade --rollback
62/// ```
63///
64/// # Safety Features
65///
66/// - **Automatic Backups**: Creates backup of current binary before upgrade
67/// - **Rollback Support**: Can restore previous version if upgrade fails
68/// - **Version Validation**: Validates version strings and availability
69/// - **Network Error Handling**: Graceful handling of connectivity issues
70/// - **Permission Checks**: Validates write access before attempting upgrade
71#[derive(Parser, Debug)]
72pub struct UpgradeArgs {
73 /// Target version to upgrade to (e.g., "0.4.0" or "v0.4.0").
74 ///
75 /// When specified, AGPM will attempt to upgrade to this specific version
76 /// instead of the latest available version. The version string can be
77 /// provided with or without the 'v' prefix.
78 ///
79 /// # Version Formats
80 ///
81 /// - `"0.4.0"` - Semantic version number
82 /// - `"v0.4.0"` - Version with 'v' prefix (GitHub tag format)
83 /// - `"0.4.0-beta.1"` - Pre-release versions
84 /// - `"0.4.0-rc.1"` - Release candidate versions
85 ///
86 /// # Behavior
87 ///
88 /// - If not specified, upgrades to the latest available version
89 /// - Version must exist as a GitHub release with binary assets
90 /// - Can be older than current version when used with `--force`
91 /// - Invalid version strings will cause the command to fail
92 ///
93 /// # Examples
94 ///
95 /// ```bash
96 /// agpm upgrade 0.4.0 # Upgrade to specific stable version
97 /// agpm upgrade v0.5.0-beta # Upgrade to beta version
98 /// agpm upgrade 0.3.0 --force # Downgrade to older version
99 /// ```
100 #[arg(value_name = "VERSION")]
101 pub version: Option<String>,
102
103 /// Check for updates without installing.
104 ///
105 /// When enabled, performs a version check against GitHub releases but
106 /// does not download or install anything. This is useful for automation,
107 /// CI/CD pipelines, or when you want to know about updates without
108 /// immediately upgrading.
109 ///
110 /// # Behavior
111 ///
112 /// - Fetches latest release information from GitHub
113 /// - Compares with current version using semantic versioning
114 /// - Displays update availability and version information
115 /// - Exits with status 0 regardless of update availability
116 /// - Caches version information for future use
117 ///
118 /// # Output Examples
119 ///
120 /// ```text
121 /// # When update is available
122 /// Update available: 0.3.14 -> 0.4.0
123 /// Run `agpm upgrade` to install the latest version
124 ///
125 /// # When up to date
126 /// You are on the latest version (0.4.0)
127 /// ```
128 ///
129 /// # Use Cases
130 ///
131 /// - **CI/CD Integration**: Check for updates in automated pipelines
132 /// - **Notification Scripts**: Alert when updates become available
133 /// - **Manual Workflow**: Check before deciding whether to upgrade
134 /// - **Development**: Verify release publication without upgrading
135 #[arg(long)]
136 pub check: bool,
137
138 /// Show current version and latest available.
139 ///
140 /// Displays comprehensive version information including the current AGPM
141 /// version and the latest available version from GitHub releases. Uses
142 /// cached version information when available to avoid unnecessary API calls.
143 ///
144 /// # Information Displayed
145 ///
146 /// - Current version of the running AGPM binary
147 /// - Latest version available on GitHub (if reachable)
148 /// - Update availability status
149 /// - Cache status (when version info was last fetched)
150 ///
151 /// # Caching Behavior
152 ///
153 /// - First checks local cache for recent version information
154 /// - Falls back to GitHub API if cache is expired or missing
155 /// - Updates cache with fresh information when fetched
156 /// - Gracefully handles network errors by using cached data
157 ///
158 /// # Output Examples
159 ///
160 /// ```text
161 /// # When update is available
162 /// Current version: 0.3.14
163 /// Latest version: 0.4.0 (update available)
164 ///
165 /// # When up to date
166 /// Current version: 0.4.0 (up to date)
167 ///
168 /// # When network is unavailable
169 /// Current version: 0.3.14
170 /// (Unable to check for latest version)
171 /// ```
172 ///
173 /// # Use Cases
174 ///
175 /// - **Quick Status Check**: See version info without upgrading
176 /// - **Troubleshooting**: Verify current version during support
177 /// - **Documentation**: Include version info in bug reports
178 /// - **Development**: Check version alignment across environments
179 #[arg(short, long)]
180 pub status: bool,
181
182 /// Force upgrade even if already on latest version.
183 ///
184 /// Bypasses version comparison checks and forces the upgrade process
185 /// to proceed regardless of the current version. This is useful for
186 /// reinstalling corrupted binaries, downgrading, or testing.
187 ///
188 /// # Behavior Changes
189 ///
190 /// - Skips "already up to date" checks
191 /// - Downloads and installs even if target version <= current version
192 /// - Enables downgrading to older versions
193 /// - Still performs all safety checks (backup, checksum verification)
194 /// - Respects other flags like `--no-backup`
195 ///
196 /// # Use Cases
197 ///
198 /// - **Reinstallation**: Fix corrupted or modified binaries
199 /// - **Downgrading**: Install older version for compatibility
200 /// - **Testing**: Verify upgrade mechanism functionality
201 /// - **Recovery**: Restore known-good version after problems
202 /// - **Development**: Install specific versions for testing
203 ///
204 /// # Safety Considerations
205 ///
206 /// Force mode still maintains safety features:
207 /// - Creates backups unless `--no-backup` is specified
208 /// - Verifies download checksums for integrity
209 /// - Validates that target version exists and has binary assets
210 /// - Provides rollback capability if installation fails
211 ///
212 /// # Examples
213 ///
214 /// ```bash
215 /// # Reinstall current version
216 /// agpm upgrade --force
217 ///
218 /// # Downgrade to older version
219 /// agpm upgrade 0.3.0 --force
220 ///
221 /// # Force upgrade to specific version
222 /// agpm upgrade 0.4.0 --force
223 /// ```
224 #[arg(short, long)]
225 pub force: bool,
226
227 /// Rollback to previous version from backup.
228 ///
229 /// Restores the AGPM binary from the backup created during the most recent
230 /// upgrade. This provides a quick recovery mechanism if the current version
231 /// has issues or if you need to revert to the previous version.
232 ///
233 /// # Rollback Process
234 ///
235 /// 1. Validates that a backup file exists
236 /// 2. Replaces current binary with backup copy
237 /// 3. Preserves file permissions and metadata
238 /// 4. Implements retry logic for Windows file locking
239 /// 5. Provides success/failure feedback
240 ///
241 /// # Requirements
242 ///
243 /// - A backup must exist from a previous upgrade
244 /// - Backup file must be readable and valid
245 /// - Write permissions to the AGPM binary location
246 /// - Current binary must not be locked by running processes
247 ///
248 /// # Error Conditions
249 ///
250 /// - No backup file found (never upgraded with backup enabled)
251 /// - Backup file is corrupted or unreadable
252 /// - Insufficient permissions to replace current binary
253 /// - File locking prevents replacement (Windows)
254 ///
255 /// # Platform Considerations
256 ///
257 /// - **Windows**: Implements retry logic for file locking issues
258 /// - **Unix**: Preserves executable permissions and ownership
259 /// - **All Platforms**: Validates backup integrity before restoration
260 ///
261 /// # Examples
262 ///
263 /// ```bash
264 /// # Simple rollback
265 /// agpm upgrade --rollback
266 ///
267 /// # Check if backup exists first
268 /// ls ~/.local/bin/agpm.backup # Unix example
269 /// agpm upgrade --rollback
270 /// ```
271 ///
272 /// # Post-Rollback
273 ///
274 /// After successful rollback:
275 /// - Previous version functionality is restored
276 /// - Version cache is not automatically cleared
277 /// - Future upgrades will work normally
278 #[arg(long)]
279 pub rollback: bool,
280
281 /// Skip creating a backup before upgrade.
282 ///
283 /// Disables the automatic backup creation that normally occurs before
284 /// upgrading the AGPM binary. This removes the safety net of being able
285 /// to rollback if the upgrade fails or the new version has issues.
286 ///
287 /// # ⚠️ WARNING
288 ///
289 /// Using this flag is **not recommended** for most users. Backups provide
290 /// crucial recovery capability with minimal overhead. Only disable backups
291 /// in specific scenarios where they cannot be created or are unnecessary.
292 ///
293 /// # When to Consider Using
294 ///
295 /// - **Disk Space Constraints**: Extremely limited storage where backup
296 /// would cause space issues
297 /// - **Permission Issues**: File system permissions prevent backup creation
298 /// - **Read-Only Installations**: When binary is in read-only file system
299 /// - **Container Environments**: Ephemeral environments where persistence
300 /// is not needed
301 /// - **Alternative Backups**: When external backup mechanisms are in place
302 ///
303 /// # Risks
304 ///
305 /// Without backups:
306 /// - No automatic rollback if upgrade fails
307 /// - Cannot use `agpm upgrade --rollback` command
308 /// - Must manually reinstall if new version has issues
309 /// - Requires external recovery mechanisms
310 ///
311 /// # Alternative Recovery
312 ///
313 /// If backups are disabled, ensure alternative recovery methods:
314 /// - Package manager installation (reinstall via `brew`, `apt`, etc.)
315 /// - Manual download from GitHub releases
316 /// - Container image rollback
317 /// - Version control system with binary tracking
318 ///
319 /// # Examples
320 ///
321 /// ```bash
322 /// # Upgrade without backup (not recommended)
323 /// agpm upgrade --no-backup
324 ///
325 /// # Force upgrade without backup
326 /// agpm upgrade 0.4.0 --force --no-backup
327 ///
328 /// # Check-only mode (backups not relevant)
329 /// agpm upgrade --check
330 /// ```
331 #[arg(long)]
332 pub no_backup: bool,
333}
334
335/// Execute the upgrade command with the provided arguments.
336///
337/// This is the main entry point for all upgrade-related operations. It handles
338/// the various upgrade modes (check, status, upgrade, rollback) and coordinates
339/// the different components (updater, backup manager, version checker) to
340/// provide a safe and reliable upgrade experience.
341///
342/// # Arguments
343///
344/// * `args` - The parsed command-line arguments containing upgrade options
345///
346/// # Command Flow
347///
348/// 1. **Initialization**: Load global config and set up cache directories
349/// 2. **Mode Detection**: Determine operation mode based on flags
350/// 3. **Component Setup**: Initialize updater, backup manager, and version checker
351/// 4. **Operation Execution**: Perform the requested operation
352/// 5. **Result Handling**: Provide user feedback and cleanup
353///
354/// # Operation Modes
355///
356/// ## Rollback Mode (`--rollback`)
357/// - Validates backup existence
358/// - Restores previous version from backup
359/// - Provides rollback status feedback
360///
361/// ## Status Mode (`--status`)
362/// - Shows current version information
363/// - Checks for latest version (cached or fresh)
364/// - Displays formatted version comparison
365///
366/// ## Check Mode (`--check`)
367/// - Fetches latest version from GitHub
368/// - Compares with current version
369/// - Shows update availability
370///
371/// ## Upgrade Mode (default)
372/// - Creates backup (unless `--no-backup`)
373/// - Downloads and installs new version
374/// - Handles success/failure scenarios
375/// - Cleans up or restores as appropriate
376///
377/// # Returns
378///
379/// - `Ok(())` - Command completed successfully
380/// - `Err(anyhow::Error)` - Command failed with detailed error information
381///
382/// # Errors
383///
384/// This function can fail for various reasons:
385///
386/// ## Network Errors
387/// - GitHub API unreachable or rate limited
388/// - Download failures for binary assets
389/// - Connectivity issues during version checks
390///
391/// ## File System Errors
392/// - Insufficient permissions to write binary or backups
393/// - Disk space exhaustion during download or backup
394/// - File locking issues (especially on Windows)
395///
396/// ## Version Errors
397/// - Target version doesn't exist on GitHub
398/// - Invalid version string format
399/// - No binary assets available for target version
400///
401/// ## Configuration Errors
402/// - Unable to load global configuration
403/// - Cache directory creation failures
404/// - Invalid executable path detection
405///
406/// # Examples
407///
408/// ```rust,no_run
409/// use agpm_cli::cli::upgrade::{UpgradeArgs, execute};
410/// use clap::Parser;
411///
412/// # async fn example() -> anyhow::Result<()> {
413/// // Parse command line arguments
414/// let args = UpgradeArgs::parse();
415///
416/// // Execute the upgrade command
417/// execute(args).await?;
418/// # Ok(())
419/// # }
420/// ```
421///
422/// # Safety Features
423///
424/// - **Automatic Backups**: Created before modifications unless disabled
425/// - **Rollback Support**: Automatic restoration on upgrade failure
426/// - **Version Validation**: Ensures target versions exist and are accessible
427/// - **Permission Checks**: Validates file system access before attempting changes
428/// - **Atomic Operations**: Uses safe file operations to minimize corruption risk
429///
430/// # User Experience
431///
432/// The function provides comprehensive user feedback:
433/// - Colored output for different message types (success, warning, error)
434/// - Progress indicators for long-running operations
435/// - Clear error messages with suggested resolution steps
436pub async fn execute(args: UpgradeArgs) -> Result<()> {
437 let _config = GlobalConfig::load().await?;
438
439 // Get the current executable path
440 let current_exe = env::current_exe().context("Failed to get current executable path")?;
441
442 // Handle rollback
443 if args.rollback {
444 return handle_rollback(¤t_exe).await;
445 }
446
447 let updater = SelfUpdater::new().force(args.force);
448 let version_checker = VersionChecker::new().await?;
449
450 // Handle status check
451 if args.status {
452 return show_status(&updater, &version_checker).await;
453 }
454
455 // Handle check for updates
456 if args.check {
457 return check_for_updates(&updater, &version_checker).await;
458 }
459
460 // Perform the upgrade
461 perform_upgrade(
462 &updater,
463 &version_checker,
464 ¤t_exe,
465 args.version.as_deref(),
466 args.no_backup,
467 )
468 .await
469}
470
471async fn handle_rollback(current_exe: &std::path::Path) -> Result<()> {
472 println!("{}", "Rolling back to previous version...".yellow());
473
474 let backup_manager = BackupManager::new(current_exe.to_path_buf());
475
476 if !backup_manager.backup_exists() {
477 bail!("No backup found. Cannot rollback.");
478 }
479
480 backup_manager.restore_backup().await.context("Failed to restore from backup")?;
481
482 println!("{}", "Successfully rolled back to previous version".green());
483
484 Ok(())
485}
486
487async fn show_status(updater: &SelfUpdater, version_checker: &VersionChecker) -> Result<()> {
488 let current_version = updater.current_version();
489
490 // Use the new check_now method which handles caching internally
491 let latest_version = match version_checker.check_now().await {
492 Ok(version) => version,
493 Err(e) => {
494 debug!("Failed to check for updates: {}", e);
495 None
496 }
497 };
498
499 let info = VersionChecker::format_version_info(current_version, latest_version.as_deref());
500 println!("{info}");
501
502 Ok(())
503}
504
505async fn check_for_updates(updater: &SelfUpdater, version_checker: &VersionChecker) -> Result<()> {
506 println!("{}", "Checking for updates...".cyan());
507
508 // Use check_now which bypasses the cache and saves the result
509 match version_checker.check_now().await {
510 Ok(Some(latest_version)) => {
511 println!(
512 "{}",
513 format!("Update available: {} -> {}", updater.current_version(), latest_version)
514 .green()
515 );
516 println!("Run `agpm upgrade` to install the latest version");
517 }
518 Ok(None) => {
519 println!(
520 "{}",
521 format!("You are on the latest version ({})", updater.current_version()).green()
522 );
523 }
524 Err(e) => {
525 bail!("Failed to check for updates: {e}");
526 }
527 }
528
529 Ok(())
530}
531
532async fn perform_upgrade(
533 updater: &SelfUpdater,
534 version_checker: &VersionChecker,
535 current_exe: &std::path::Path,
536 target_version: Option<&str>,
537 no_backup: bool,
538) -> Result<()> {
539 // Create backup unless explicitly skipped
540 let backup_manager = if no_backup {
541 None
542 } else {
543 println!("{}", "Creating backup...".cyan());
544 let manager = BackupManager::new(current_exe.to_path_buf());
545 manager.create_backup().await.context("Failed to create backup")?;
546 Some(manager)
547 };
548
549 // Perform the upgrade
550 let upgrade_msg = if let Some(version) = target_version {
551 format!("Upgrading to version {version}...").cyan()
552 } else {
553 "Upgrading to latest version...".cyan()
554 };
555 println!("{upgrade_msg}");
556
557 let result = if let Some(version) = target_version {
558 updater.update_to_version(version).await
559 } else {
560 updater.update_to_latest().await
561 };
562
563 match result {
564 Ok(true) => {
565 // Clear version cache after successful update
566 version_checker.clear_cache().await?;
567
568 println!("{}", "Upgrade completed successfully!".green());
569
570 // Clean up backup after successful upgrade
571 if let Some(manager) = backup_manager
572 && let Err(e) = manager.cleanup_backup().await
573 {
574 debug!("Failed to cleanup backup: {}", e);
575 }
576 }
577 Ok(false) => {
578 println!(
579 "{}",
580 format!("Already on the latest version ({})", updater.current_version()).green()
581 );
582 }
583 Err(e) => {
584 // Attempt to restore from backup on failure
585 if let Some(manager) = backup_manager {
586 println!("{}", "Upgrade failed. Attempting to restore backup...".red());
587 if let Err(restore_err) = manager.restore_backup().await {
588 eprintln!("{}", format!("Failed to restore backup: {restore_err}").red());
589 eprintln!("Backup is located at: {}", manager.backup_path().display());
590 } else {
591 println!("{}", "Successfully restored from backup".green());
592 }
593 }
594 bail!("Upgrade failed: {e}");
595 }
596 }
597
598 Ok(())
599}