dx_forge/
auto_update.rs

1//! Auto-Update System with Green Traffic Support
2//!
3//! Provides automatic updates for green traffic changes, version conflict detection,
4//! update notifications, and rollback capability.
5
6use anyhow::{anyhow, Result};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::path::{Path, PathBuf};
10use chrono::{DateTime, Utc};
11
12use crate::version::{Version, ToolInfo, ToolRegistry};
13
14/// Traffic level for updates (simplified version)
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub enum TrafficLevel {
17    /// Green traffic - safe for auto-update
18    Green,
19    /// Yellow traffic - requires review
20    Yellow,
21    /// Red traffic - requires manual intervention
22    Red,
23}
24
25/// Update status
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub enum UpdateStatus {
28    /// Update available but not applied
29    Available,
30    /// Update is being downloaded
31    Downloading,
32    /// Update is being applied
33    Applying,
34    /// Update successfully applied
35    Applied,
36    /// Update failed
37    Failed(String),
38    /// Update requires manual intervention
39    RequiresManual,
40}
41
42/// Update notification
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct UpdateNotification {
45    pub tool_name: String,
46    pub current_version: Version,
47    pub new_version: Version,
48    pub traffic_level: TrafficLevel,
49    pub status: UpdateStatus,
50    pub timestamp: DateTime<Utc>,
51    pub message: String,
52}
53
54/// Backup information for rollback
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct Backup {
57    pub id: String,
58    pub tool_name: String,
59    pub version: Version,
60    pub created_at: DateTime<Utc>,
61    pub backup_path: PathBuf,
62}
63
64/// Auto-update manager
65pub struct AutoUpdateManager {
66    registry: ToolRegistry,
67    backups: HashMap<String, Vec<Backup>>,
68    notifications: Vec<UpdateNotification>,
69    auto_update_enabled: bool,
70}
71
72impl AutoUpdateManager {
73    /// Create a new auto-update manager
74    pub fn new(forge_dir: &Path) -> Result<Self> {
75        let registry = ToolRegistry::new(forge_dir)?;
76        
77        Ok(Self {
78            registry,
79            backups: HashMap::new(),
80            notifications: Vec::new(),
81            auto_update_enabled: true,
82        })
83    }
84
85    /// Enable or disable auto-updates
86    pub fn set_auto_update(&mut self, enabled: bool) {
87        self.auto_update_enabled = enabled;
88    }
89
90    /// Check for updates for a specific tool
91    pub fn check_update(&self, tool_name: &str, latest_version: Version) -> Option<UpdateNotification> {
92        if let Some(current_version) = self.registry.version(tool_name) {
93            if &latest_version > current_version {
94                return Some(UpdateNotification {
95                    tool_name: tool_name.to_string(),
96                    current_version: current_version.clone(),
97                    new_version: latest_version.clone(),
98                    traffic_level: self.determine_traffic_level(current_version, &latest_version),
99                    status: UpdateStatus::Available,
100                    timestamp: Utc::now(),
101                    message: format!("Update available: {} -> {}", current_version, latest_version),
102                });
103            }
104        }
105        None
106    }
107
108    /// Determine traffic level for an update
109    fn determine_traffic_level(&self, current: &Version, new: &Version) -> TrafficLevel {
110        // Green: patch updates
111        if current.major == new.major && current.minor == new.minor {
112            return TrafficLevel::Green;
113        }
114
115        // Yellow: minor updates
116        if current.major == new.major {
117            return TrafficLevel::Yellow;
118        }
119
120        // Red: major updates
121        TrafficLevel::Red
122    }
123
124    /// Apply update if it's a green traffic update
125    pub fn process_update(&mut self, notification: &UpdateNotification) -> Result<()> {
126        if !self.auto_update_enabled {
127            return Ok(());
128        }
129
130        match notification.traffic_level {
131            TrafficLevel::Green => {
132                println!("🟢 Auto-applying green traffic update: {} {} -> {}",
133                    notification.tool_name,
134                    notification.current_version,
135                    notification.new_version
136                );
137                self.apply_update(notification)?;
138            },
139            TrafficLevel::Yellow => {
140                println!("🟡 Yellow traffic update available: {} {} -> {} (requires review)",
141                    notification.tool_name,
142                    notification.current_version,
143                    notification.new_version
144                );
145                self.add_notification(notification.clone());
146            },
147            TrafficLevel::Red => {
148                println!("🔴 Red traffic update available: {} {} -> {} (requires manual intervention)",
149                    notification.tool_name,
150                    notification.current_version,
151                    notification.new_version
152                );
153                self.add_notification(notification.clone());
154            },
155        }
156
157        Ok(())
158    }
159
160    /// Apply an update
161    fn apply_update(&mut self, notification: &UpdateNotification) -> Result<()> {
162        // Create backup before applying update
163        self.create_backup(&notification.tool_name, &notification.current_version)?;
164
165        // In a real implementation, this would download and install the update
166        // For now, we'll just update the registry
167        println!("  ✓ Created backup");
168        println!("  ⬇ Downloading update...");
169        println!("  ✓ Applying update...");
170
171        // Update the registry (simulated)
172        let _tool_info = ToolInfo {
173            name: notification.tool_name.clone(),
174            version: notification.new_version.clone(),
175            installed_at: Utc::now(),
176            source: crate::version::ToolSource::Crate {
177                version: notification.new_version.to_string(),
178            },
179            dependencies: HashMap::new(),
180        };
181
182        // In reality, we'd update through the registry methods
183        println!("  ✓ Update applied successfully");
184
185        // Add success notification
186        self.add_notification(UpdateNotification {
187            status: UpdateStatus::Applied,
188            timestamp: Utc::now(),
189            message: format!("Successfully updated to {}", notification.new_version),
190            ..notification.clone()
191        });
192
193        Ok(())
194    }
195
196    /// Create a backup for rollback
197    fn create_backup(&mut self, tool_name: &str, version: &Version) -> Result<()> {
198        let backup_id = uuid::Uuid::new_v4().to_string();
199        let backup_path = PathBuf::from(format!(".dx/forge/backups/{}/{}", tool_name, backup_id));
200
201        // In a real implementation, this would copy the tool files
202        std::fs::create_dir_all(&backup_path)?;
203
204        let backup = Backup {
205            id: backup_id,
206            tool_name: tool_name.to_string(),
207            version: version.clone(),
208            created_at: Utc::now(),
209            backup_path,
210        };
211
212        self.backups
213            .entry(tool_name.to_string())
214            .or_insert_with(Vec::new)
215            .push(backup);
216
217        Ok(())
218    }
219
220    /// Rollback to a previous version
221    pub fn rollback(&mut self, tool_name: &str) -> Result<()> {
222        let backups = self.backups
223            .get_mut(tool_name)
224            .ok_or_else(|| anyhow!("No backups found for {}", tool_name))?;
225
226        let backup = backups.pop()
227            .ok_or_else(|| anyhow!("No backups available for {}", tool_name))?;
228
229        println!("🔄 Rolling back {} to version {}", tool_name, backup.version);
230        println!("  ✓ Restoring from backup: {}", backup.id);
231
232        // In a real implementation, this would restore the files from backup
233        // and update the registry
234        println!("  ✓ Rollback complete");
235
236        // Add notification
237        self.add_notification(UpdateNotification {
238            tool_name: tool_name.to_string(),
239            current_version: backup.version.clone(),
240            new_version: backup.version.clone(),
241            traffic_level: TrafficLevel::Green,
242            status: UpdateStatus::Applied,
243            timestamp: Utc::now(),
244            message: format!("Rolled back to version {}", backup.version),
245        });
246
247        Ok(())
248    }
249
250    /// Add a notification
251    fn add_notification(&mut self, notification: UpdateNotification) {
252        self.notifications.push(notification);
253
254        // Keep only last 100 notifications
255        if self.notifications.len() > 100 {
256            self.notifications.remove(0);
257        }
258    }
259
260    /// Get all notifications
261    pub fn get_notifications(&self) -> &[UpdateNotification] {
262        &self.notifications
263    }
264
265    /// Get pending updates
266    pub fn get_pending_updates(&self) -> Vec<&UpdateNotification> {
267        self.notifications
268            .iter()
269            .filter(|n| n.status == UpdateStatus::Available || n.status == UpdateStatus::RequiresManual)
270            .collect()
271    }
272
273    /// Clear old notifications
274    pub fn clear_old_notifications(&mut self, days: i64) {
275        let cutoff = Utc::now() - chrono::Duration::days(days);
276        self.notifications.retain(|n| n.timestamp > cutoff);
277    }
278
279    /// Detect version conflicts
280    pub fn detect_conflicts(&self) -> Vec<String> {
281        let mut conflicts = Vec::new();
282
283        for tool_info in self.registry.list() {
284            if let Ok(missing_deps) = self.registry.check_dependencies(&tool_info.name) {
285                conflicts.extend(missing_deps);
286            }
287        }
288
289        conflicts
290    }
291
292    /// Get backup history for a tool
293    pub fn get_backups(&self, tool_name: &str) -> Vec<&Backup> {
294        self.backups
295            .get(tool_name)
296            .map(|backups| backups.iter().collect())
297            .unwrap_or_default()
298    }
299
300    /// Clean old backups
301    pub fn clean_old_backups(&mut self, days: i64) -> Result<()> {
302        let cutoff = Utc::now() - chrono::Duration::days(days);
303
304        for backups in self.backups.values_mut() {
305            backups.retain(|backup| {
306                if backup.created_at < cutoff {
307                    // Delete backup directory
308                    let _  = std::fs::remove_dir_all(&backup.backup_path);
309                    false
310                } else {
311                    true
312                }
313            });
314        }
315
316        Ok(())
317    }
318}
319
320/// Update preference for notifications
321#[derive(Debug, Clone, Serialize, Deserialize)]
322pub struct UpdatePreference {
323    pub auto_update_green: bool,
324    pub notify_yellow: bool,
325    pub notify_red: bool,
326    pub email_notifications: bool,
327}
328
329impl Default for UpdatePreference {
330    fn default() -> Self {
331        Self {
332            auto_update_green: true,
333            notify_yellow: true,
334            notify_red: true,
335            email_notifications: false,
336        }
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343
344    #[test]
345    fn test_determine_traffic_level() {
346        let manager = AutoUpdateManager {
347            registry: ToolRegistry::new(Path::new(".dx/forge")).unwrap(),
348            backups: HashMap::new(),
349            notifications: Vec::new(),
350            auto_update_enabled: true,
351        };
352
353        // Patch update (green)
354        let current = Version::new(1, 2, 3);
355        let new = Version::new(1, 2, 4);
356        assert_eq!(manager.determine_traffic_level(&current, &new), TrafficLevel::Green);
357
358        // Minor update (yellow)
359        let new = Version::new(1, 3, 0);
360        assert_eq!(manager.determine_traffic_level(&current, &new), TrafficLevel::Yellow);
361
362        // Major update (red)
363        let new = Version::new(2, 0, 0);
364        assert_eq!(manager.determine_traffic_level(&current, &new), TrafficLevel::Red);
365    }
366}