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