1use crate::defaults::get_config_dir;
7use anyhow::{Context, Result};
8use semver::Version;
9use serde::{Deserialize, Serialize};
10use std::fs;
11use std::path::PathBuf;
12
13#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Default)]
15#[serde(rename_all = "lowercase")]
16pub enum ReleaseChannel {
17 #[default]
19 Stable,
20 Beta,
22 Nightly,
24}
25
26impl std::fmt::Display for ReleaseChannel {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 match self {
29 Self::Stable => write!(f, "stable"),
30 Self::Beta => write!(f, "beta"),
31 Self::Nightly => write!(f, "nightly"),
32 }
33 }
34}
35
36#[derive(Debug, Clone, Deserialize, Serialize, Default)]
38pub struct MirrorConfig {
39 pub primary: Option<String>,
41 #[serde(default)]
43 pub fallbacks: Vec<String>,
44 #[serde(default = "default_true")]
46 pub geo_select: bool,
47}
48
49#[derive(Debug, Clone, Deserialize, Serialize, Default)]
51pub struct VersionPin {
52 pub version: Option<Version>,
54 pub reason: Option<String>,
56 #[serde(default)]
58 pub auto_unpin: bool,
59}
60
61#[derive(Debug, Clone, Deserialize, Serialize)]
63pub struct UpdateConfig {
64 #[serde(default)]
66 pub channel: ReleaseChannel,
67
68 #[serde(default)]
70 pub pin: Option<VersionPin>,
71
72 #[serde(default)]
74 pub mirrors: MirrorConfig,
75
76 #[serde(default = "default_check_interval")]
78 pub check_interval_hours: u64,
79
80 #[serde(default = "default_download_timeout")]
82 pub download_timeout_secs: u64,
83
84 #[serde(default = "default_true")]
86 pub keep_backup: bool,
87
88 #[serde(default)]
90 pub auto_rollback: bool,
91}
92
93impl Default for UpdateConfig {
94 fn default() -> Self {
95 Self {
96 channel: ReleaseChannel::Stable,
97 pin: None,
98 mirrors: MirrorConfig::default(),
99 check_interval_hours: default_check_interval(),
100 download_timeout_secs: default_download_timeout(),
101 keep_backup: true,
102 auto_rollback: false,
103 }
104 }
105}
106
107fn default_true() -> bool {
108 true
109}
110
111fn default_check_interval() -> u64 {
112 24 }
114
115fn default_download_timeout() -> u64 {
116 300 }
118
119impl UpdateConfig {
120 pub fn load() -> Result<Self> {
122 let config_path = Self::config_path().context("Failed to determine update config path")?;
123
124 if !config_path.exists() {
125 return Ok(Self::default());
127 }
128
129 let content = fs::read_to_string(&config_path)
130 .with_context(|| format!("Failed to read update config: {}", config_path.display()))?;
131
132 let config: UpdateConfig = toml::from_str(&content)
133 .with_context(|| format!("Failed to parse update config: {}", config_path.display()))?;
134
135 Ok(config)
136 }
137
138 pub fn save(&self) -> Result<()> {
140 let config_path = Self::config_path().context("Failed to determine update config path")?;
141
142 if let Some(parent) = config_path.parent() {
144 fs::create_dir_all(parent).with_context(|| {
145 format!("Failed to create config directory: {}", parent.display())
146 })?;
147 }
148
149 let content = toml::to_string_pretty(self).context("Failed to serialize update config")?;
150
151 fs::write(&config_path, content)
152 .with_context(|| format!("Failed to write update config: {}", config_path.display()))?;
153
154 Ok(())
155 }
156
157 pub fn config_path() -> Result<PathBuf> {
159 let config_dir = get_config_dir().context("Failed to get config directory")?;
160 Ok(config_dir.join("update.toml"))
161 }
162
163 pub fn is_pinned(&self) -> bool {
165 self.pin.as_ref().is_some_and(|p| p.version.is_some())
166 }
167
168 pub fn pinned_version(&self) -> Option<&Version> {
170 self.pin.as_ref().and_then(|p| p.version.as_ref())
171 }
172
173 pub fn set_pin(&mut self, version: Version, reason: Option<String>, auto_unpin: bool) {
175 self.pin = Some(VersionPin {
176 version: Some(version),
177 reason,
178 auto_unpin,
179 });
180 }
181
182 pub fn clear_pin(&mut self) {
184 self.pin = None;
185 }
186
187 pub fn is_check_due(&self, last_check: Option<std::time::SystemTime>) -> bool {
189 if self.check_interval_hours == 0 {
190 return false; }
192
193 let Some(last_check) = last_check else {
194 return true; };
196
197 let elapsed = std::time::SystemTime::now()
198 .duration_since(last_check)
199 .unwrap_or_default();
200
201 elapsed >= std::time::Duration::from_secs(self.check_interval_hours * 3600)
202 }
203}
204
205pub fn create_example_config() -> String {
207 r#"# VT Code Update Configuration
208# Location: ~/.vtcode/update.toml
209
210# Release channel to follow
211# Options: stable (default), beta, nightly
212channel = "stable"
213
214# Version pinning (optional)
215# Uncomment to pin to a specific version
216# [pin]
217# version = "0.85.3"
218# reason = "Waiting for bug fix in next release"
219# auto_unpin = false
220
221# Download mirrors (optional)
222# [mirrors]
223# primary = "https://github.com/vinhnx/vtcode/releases"
224# fallbacks = [
225# "https://mirror.example.com/vtcode",
226# ]
227# geo_select = true
228
229# Auto-update check interval in hours (0 = disable)
230check_interval_hours = 24
231
232# Download timeout in seconds
233download_timeout_secs = 300
234
235# Keep backup of previous version after update
236keep_backup = true
237
238# Auto-rollback on startup if new version fails
239auto_rollback = false
240"#
241 .to_string()
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 #[test]
249 fn test_default_config() {
250 let config = UpdateConfig::default();
251 assert_eq!(config.channel, ReleaseChannel::Stable);
252 assert_eq!(config.check_interval_hours, 24);
253 assert_eq!(config.download_timeout_secs, 300);
254 assert!(config.keep_backup);
255 assert!(!config.auto_rollback);
256 }
257
258 #[test]
259 fn test_release_channel_display() {
260 assert_eq!(ReleaseChannel::Stable.to_string(), "stable");
261 assert_eq!(ReleaseChannel::Beta.to_string(), "beta");
262 assert_eq!(ReleaseChannel::Nightly.to_string(), "nightly");
263 }
264
265 #[test]
266 fn test_version_pin() {
267 let mut config = UpdateConfig::default();
268 let version = Version::parse("0.85.3").unwrap();
269 config.set_pin(version.clone(), Some("Testing".to_string()), false);
270
271 assert!(config.is_pinned());
272 assert_eq!(config.pinned_version(), Some(&version));
273
274 config.clear_pin();
275 assert!(!config.is_pinned());
276 }
277}