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(&current_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        &current_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}