1use 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub enum TrafficLevel {
17 Green,
19 Yellow,
21 Red,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub enum UpdateStatus {
28 Available,
30 Downloading,
32 Applying,
34 Applied,
36 Failed(String),
38 RequiresManual,
40}
41
42#[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#[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
64pub 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 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 pub fn set_auto_update(&mut self, enabled: bool) {
87 self.auto_update_enabled = enabled;
88 }
89
90 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 fn determine_traffic_level(&self, current: &Version, new: &Version) -> TrafficLevel {
110 if current.major == new.major && current.minor == new.minor {
112 return TrafficLevel::Green;
113 }
114
115 if current.major == new.major {
117 return TrafficLevel::Yellow;
118 }
119
120 TrafficLevel::Red
122 }
123
124 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 fn apply_update(&mut self, notification: &UpdateNotification) -> Result<()> {
162 self.create_backup(¬ification.tool_name, ¬ification.current_version)?;
164
165 println!(" ✓ Created backup");
168 println!(" ⬇ Downloading update...");
169 println!(" ✓ Applying update...");
170
171 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 println!(" ✓ Update applied successfully");
184
185 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 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 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 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 println!(" ✓ Rollback complete");
235
236 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 fn add_notification(&mut self, notification: UpdateNotification) {
252 self.notifications.push(notification);
253
254 if self.notifications.len() > 100 {
256 self.notifications.remove(0);
257 }
258 }
259
260 pub fn get_notifications(&self) -> &[UpdateNotification] {
262 &self.notifications
263 }
264
265 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 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 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 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 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 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#[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 let current = Version::new(1, 2, 3);
355 let new = Version::new(1, 2, 4);
356 assert_eq!(manager.determine_traffic_level(¤t, &new), TrafficLevel::Green);
357
358 let new = Version::new(1, 3, 0);
360 assert_eq!(manager.determine_traffic_level(¤t, &new), TrafficLevel::Yellow);
361
362 let new = Version::new(2, 0, 0);
364 assert_eq!(manager.determine_traffic_level(¤t, &new), TrafficLevel::Red);
365 }
366}