From 5b750038b9bdd4e0e5616b77a405e6746b217689 Mon Sep 17 00:00:00 2001 From: echobt Date: Thu, 5 Feb 2026 14:23:37 +0000 Subject: [PATCH 1/2] feat(tui): add auto-update notification system Add background update checking on TUI startup with visual notification: - Add UpdateStatus enum tracking: Checking, Available, Downloading, Downloaded, UpToDate, and Error states - Integrate update check in app runner on startup (background async) - Display update banner in MinimalSessionView input area showing: - 'Checking for updates...' during check - 'A new version (vX.X.X) is available' when update found - 'Downloading update...' during download - 'Restart to apply update' after download completes - Add security hardening to cortex-update: - Enforce HTTPS for update URLs (reject insecure except localhost) - Add path traversal protection in archive extraction - Use cryptographically secure random bytes for temp directories - Fix TOCTOU race condition in tar/zip extraction - Add SAFETY comments for unsafe Windows API calls - Address clippy warnings and code quality improvements --- Cargo.lock | 2 + src/cortex-tui/Cargo.toml | 3 + src/cortex-tui/src/app/mod.rs | 2 +- src/cortex-tui/src/app/state.rs | 107 ++++++++++++++++++ .../src/runner/app_runner/runner.rs | 38 ++++++- .../src/views/minimal_session/rendering.rs | 62 ++++++++++ .../src/views/minimal_session/view.rs | 23 +++- src/cortex-update/Cargo.toml | 3 + src/cortex-update/src/api.rs | 32 +++++- src/cortex-update/src/config.rs | 15 ++- src/cortex-update/src/download.rs | 18 +-- src/cortex-update/src/error.rs | 4 + src/cortex-update/src/install.rs | 105 +++++++++++++++-- src/cortex-update/src/lib.rs | 2 +- src/cortex-update/src/manager.rs | 14 +-- src/cortex-update/src/method.rs | 16 +-- src/cortex-update/src/verify.rs | 2 + src/cortex-update/src/version.rs | 1 + 18 files changed, 403 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5d14bb1..c810cd4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1669,6 +1669,7 @@ dependencies = [ "cortex-protocol", "cortex-tui-capture", "cortex-tui-components", + "cortex-update", "crossterm", "dirs 6.0.0", "futures", @@ -1828,6 +1829,7 @@ dependencies = [ "dirs 6.0.0", "flate2", "futures", + "getrandom 0.2.17", "hex", "libc", "reqwest 0.13.1", diff --git a/src/cortex-tui/Cargo.toml b/src/cortex-tui/Cargo.toml index 6ad2322..c7dee93 100644 --- a/src/cortex-tui/Cargo.toml +++ b/src/cortex-tui/Cargo.toml @@ -16,6 +16,9 @@ audio = ["dep:rodio"] # Cortex TUI core (the TUI engine) cortex-core = { path = "../cortex-core" } +# Auto-update system +cortex-update = { path = "../cortex-update" } + # Centralized TUI components cortex-tui-components = { path = "../cortex-tui-components" } diff --git a/src/cortex-tui/src/app/mod.rs b/src/cortex-tui/src/app/mod.rs index 7b9c9fe..852cd40 100644 --- a/src/cortex-tui/src/app/mod.rs +++ b/src/cortex-tui/src/app/mod.rs @@ -15,7 +15,7 @@ mod types; pub use approval::{ApprovalState, PendingToolResult}; pub use autocomplete::{AutocompleteItem, AutocompleteState}; pub use session::{ActiveModal, SessionSummary}; -pub use state::AppState; +pub use state::{AppState, UpdateStatus}; pub use streaming::StreamingState; pub use subagent::{ SubagentDisplayStatus, SubagentTaskDisplay, SubagentTodoItem, SubagentTodoStatus, diff --git a/src/cortex-tui/src/app/state.rs b/src/cortex-tui/src/app/state.rs index d79a5f9..f60659d 100644 --- a/src/cortex-tui/src/app/state.rs +++ b/src/cortex-tui/src/app/state.rs @@ -7,6 +7,9 @@ use cortex_core::{ style::ThemeColors, widgets::{CortexInput, Message}, }; +// DownloadProgress and UpdateInfo are used in future download tracking feature +#[allow(unused_imports)] +use cortex_update::{DownloadProgress, UpdateInfo}; use uuid::Uuid; use crate::permissions::PermissionMode; @@ -22,6 +25,68 @@ use super::streaming::StreamingState; use super::subagent::SubagentTaskDisplay; use super::types::{AppView, FocusTarget, OperationMode}; +/// Status of the auto-update system +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub enum UpdateStatus { + /// No update check performed yet + #[default] + NotChecked, + /// Currently checking for updates + Checking, + /// An update is available + Available { + /// The new version available + version: String, + }, + /// Currently downloading the update + Downloading { + /// The version being downloaded + version: String, + /// Download progress percentage (0-100) + progress: u8, + }, + /// Download complete, restart required + ReadyToRestart { + /// The version that was downloaded + version: String, + }, + /// Update check failed (network error, etc.) + CheckFailed { + /// Error message + error: String, + }, + /// Already on the latest version + UpToDate, +} + +impl UpdateStatus { + /// Returns true if an update notification should be shown + pub fn should_show_banner(&self) -> bool { + matches!( + self, + UpdateStatus::Available { .. } + | UpdateStatus::Downloading { .. } + | UpdateStatus::ReadyToRestart { .. } + ) + } + + /// Get the banner text for the current status + pub fn banner_text(&self) -> Option { + match self { + UpdateStatus::Available { version } => { + Some(format!("A new version ({}) is available", version)) + } + UpdateStatus::Downloading { progress, .. } => { + Some(format!("Downloading update... {}%", progress)) + } + UpdateStatus::ReadyToRestart { .. } => { + Some("You must restart to run the latest version".to_string()) + } + _ => None, + } + } +} + /// Main application state pub struct AppState { pub view: AppView, @@ -172,6 +237,10 @@ pub struct AppState { pub user_email: Option, /// Organization name for welcome screen pub org_name: Option, + /// Current update status for the banner notification + pub update_status: UpdateStatus, + /// Cached update info when an update is available + pub update_info: Option, } impl AppState { @@ -272,6 +341,8 @@ impl AppState { user_name: None, user_email: None, org_name: None, + update_status: UpdateStatus::default(), + update_info: None, } } @@ -679,3 +750,39 @@ impl AppState { self.diff_scroll = (self.diff_scroll + delta).max(0); } } + +// ============================================================================ +// APPSTATE METHODS - Update Status +// ============================================================================ + +impl AppState { + /// Set the update status + pub fn set_update_status(&mut self, status: UpdateStatus) { + self.update_status = status; + } + + /// Set update info when an update is available + pub fn set_update_info(&mut self, info: Option) { + self.update_info = info; + } + + /// Check if an update banner should be shown + pub fn should_show_update_banner(&self) -> bool { + self.update_status.should_show_banner() + } + + /// Get the update banner text if one should be shown + pub fn get_update_banner_text(&self) -> Option { + self.update_status.banner_text() + } + + /// Update download progress + pub fn update_download_progress(&mut self, progress: u8) { + if let UpdateStatus::Downloading { version, .. } = &self.update_status { + self.update_status = UpdateStatus::Downloading { + version: version.clone(), + progress, + }; + } + } +} diff --git a/src/cortex-tui/src/runner/app_runner/runner.rs b/src/cortex-tui/src/runner/app_runner/runner.rs index 4ae39b2..e79ac37 100644 --- a/src/cortex-tui/src/runner/app_runner/runner.rs +++ b/src/cortex-tui/src/runner/app_runner/runner.rs @@ -4,7 +4,7 @@ use super::auth_status::AuthStatus; use super::exit_info::{AppExitInfo, ExitReason}; use super::trusted_workspaces::{is_workspace_trusted, mark_workspace_trusted}; -use crate::app::AppState; +use crate::app::{AppState, UpdateStatus}; use crate::bridge::SessionBridge; use crate::providers::ProviderManager; use crate::runner::event_loop::EventLoop; @@ -15,7 +15,9 @@ use anyhow::Result; use cortex_engine::Config; use cortex_login::{CredentialsStoreMode, load_auth, logout_with_fallback}; use cortex_protocol::ConversationId; +use cortex_update::UpdateManager; use std::path::PathBuf; +use std::time::Duration; use tracing; // ============================================================================ @@ -552,6 +554,23 @@ impl AppRunner { let session_history_task = tokio::task::spawn_blocking(|| CortexSession::list_recent(50).ok()); + // 2. Background update check task - check for new versions without blocking startup + let update_check_task = tokio::spawn(async move { + match UpdateManager::new() { + Ok(manager) => match manager.check_update().await { + Ok(info) => info, + Err(e) => { + tracing::debug!("Update check failed: {}", e); + None + } + }, + Err(e) => { + tracing::debug!("Failed to create update manager: {}", e); + None + } + } + }); + // 3. Models prefetch and session validation - spawn in background // We use a channel to receive results and update provider_manager later let models_and_validation_task = { @@ -640,6 +659,23 @@ impl AppRunner { ); } + // Collect update check result (with short timeout to not block startup) + if let Ok(Ok(Some(info))) = + tokio::time::timeout(Duration::from_secs(3), update_check_task).await + { + tracing::info!( + "Update available: {} -> {}", + info.current_version, + info.latest_version + ); + app_state.set_update_status(UpdateStatus::Available { + version: info.latest_version.clone(), + }); + app_state.set_update_info(Some(info)); + } else { + tracing::debug!("Update check did not complete in time or no update available"); + } + // Check validation result (with short timeout - don't block TUI) // We'll handle models update after event loop is created let validation_result = tokio::time::timeout( diff --git a/src/cortex-tui/src/views/minimal_session/rendering.rs b/src/cortex-tui/src/views/minimal_session/rendering.rs index 7381603..892c90c 100644 --- a/src/cortex-tui/src/views/minimal_session/rendering.rs +++ b/src/cortex-tui/src/views/minimal_session/rendering.rs @@ -960,3 +960,65 @@ pub fn render_motd_compact( Paragraph::new(lines).render(text_area, buf); } + +/// Renders an update notification banner above the input box. +/// Shows different states: Available, Downloading (with progress), ReadyToRestart +pub fn render_update_banner( + area: Rect, + buf: &mut Buffer, + colors: &AdaptiveColors, + update_status: &crate::app::UpdateStatus, +) { + use crate::app::UpdateStatus; + + if area.is_empty() || area.height < 1 { + return; + } + + let (icon, text, style) = match update_status { + UpdateStatus::Available { version } => { + let icon = "↑"; + let text = format!(" A new version ({}) is available ", version); + let style = Style::default() + .fg(colors.accent) + .add_modifier(Modifier::BOLD); + (icon, text, style) + } + UpdateStatus::Downloading { + version: _, + progress, + } => { + let icon = "⟳"; + let text = format!(" Downloading update... {}% ", progress); + let style = Style::default().fg(colors.warning); + (icon, text, style) + } + UpdateStatus::ReadyToRestart { version: _ } => { + let icon = "✓"; + let text = " You must restart to run the latest version ".to_string(); + let style = Style::default() + .fg(colors.success) + .add_modifier(Modifier::BOLD); + (icon, text, style) + } + _ => return, // Don't render for other states + }; + + // Calculate banner width + let banner_width = (icon.len() + text.len() + 2) as u16; // +2 for spacing + + // Position at left side of the area with some padding + let x = area.x + 2; + let y = area.y; + + // Ensure we don't overflow + if x + banner_width > area.right() { + return; + } + + // Render icon + buf.set_string(x, y, icon, style); + + // Render text + buf.set_string(x + icon.len() as u16 + 1, y, &text, style); +} diff --git a/src/cortex-tui/src/views/minimal_session/view.rs b/src/cortex-tui/src/views/minimal_session/view.rs index 725dd0e..a03f51f 100644 --- a/src/cortex-tui/src/views/minimal_session/view.rs +++ b/src/cortex-tui/src/views/minimal_session/view.rs @@ -569,6 +569,8 @@ impl<'a> Widget for MinimalSessionView<'a> { self.app_state.autocomplete.visible && self.app_state.autocomplete.has_items(); let autocomplete_height: u16 = if autocomplete_visible { 10 } else { 0 }; let status_height: u16 = if is_task_running { 1 } else { 0 }; + let show_update_banner = self.app_state.should_show_update_banner(); + let update_banner_height: u16 = if show_update_banner { 1 } else { 0 }; let input_height: u16 = 3; let hints_height: u16 = 1; @@ -584,7 +586,12 @@ impl<'a> Widget for MinimalSessionView<'a> { layout.gap(1); // Calculate available height for scrollable content (before input/hints) - let bottom_reserved = status_height + input_height + autocomplete_height + hints_height + 2; // +2 for gaps + let bottom_reserved = status_height + + update_banner_height + + input_height + + autocomplete_height + + hints_height + + 2; // +2 for gaps let available_height = area.height.saturating_sub(1 + bottom_reserved); // 1 for top margin // Render scrollable content area (welcome cards + messages together) @@ -608,7 +615,19 @@ impl<'a> Widget for MinimalSessionView<'a> { next_y += status_height; } - // 6. Input area - follows status (or content if no status) + // 5.5 Update banner (if applicable) - above input + if show_update_banner { + let banner_area = Rect::new(area.x, next_y, area.width, update_banner_height); + super::rendering::render_update_banner( + banner_area, + buf, + &self.colors, + &self.app_state.update_status, + ); + next_y += update_banner_height; + } + + // 6. Input area - follows update banner (or status if no banner) let input_y = next_y; let input_area = Rect::new(area.x, input_y, area.width, input_height); diff --git a/src/cortex-update/Cargo.toml b/src/cortex-update/Cargo.toml index 49d33f4..2395f46 100644 --- a/src/cortex-update/Cargo.toml +++ b/src/cortex-update/Cargo.toml @@ -39,6 +39,9 @@ flate2 = "1.0" zip = "2.2" tar = "0.4" +# Secure random +getrandom = "0.2" + # Self-replacement self-replace = "1.5" diff --git a/src/cortex-update/src/api.rs b/src/cortex-update/src/api.rs index 84eb456..6424f96 100644 --- a/src/cortex-update/src/api.rs +++ b/src/cortex-update/src/api.rs @@ -90,16 +90,31 @@ impl CortexSoftwareClient { impl CortexSoftwareClient { /// Create a new client with the default URL. pub fn new() -> Self { - Self::with_url(SOFTWARE_URL.to_string()) + // Default URL is known to be HTTPS, so unwrap is safe + Self::with_url(SOFTWARE_URL.to_string()).expect("Default SOFTWARE_URL must be HTTPS") } /// Create a new client with a custom URL. - pub fn with_url(base_url: String) -> Self { + /// + /// # Errors + /// + /// Returns `UpdateError::InsecureUrl` if the URL does not use HTTPS + /// (except for localhost/127.0.0.1 which are allowed for development). + pub fn with_url(base_url: String) -> UpdateResult { + // Validate URL uses HTTPS for security (allow http for localhost development only) + let url_lower = base_url.to_lowercase(); + if !url_lower.starts_with("https://") + && !url_lower.starts_with("http://localhost") + && !url_lower.starts_with("http://127.0.0.1") + { + return Err(UpdateError::InsecureUrl { url: base_url }); + } + let client = create_client_builder() .build() .unwrap_or_else(|_| Client::new()); - Self { client, base_url } + Ok(Self { client, base_url }) } /// Get the latest release for a channel. @@ -204,6 +219,17 @@ impl CortexSoftwareClient { where F: FnMut(u64, u64), // (downloaded, total) { + // Validate asset URL uses HTTPS (allow localhost for development) + let url_lower = asset.url.to_lowercase(); + if !url_lower.starts_with("https://") + && !url_lower.starts_with("http://localhost") + && !url_lower.starts_with("http://127.0.0.1") + { + return Err(UpdateError::InsecureUrl { + url: asset.url.clone(), + }); + } + let response = self.client .get(&asset.url) diff --git a/src/cortex-update/src/config.rs b/src/cortex-update/src/config.rs index b70b43a..5cee4d3 100644 --- a/src/cortex-update/src/config.rs +++ b/src/cortex-update/src/config.rs @@ -106,10 +106,17 @@ impl UpdateConfig { .map(|h| h.join(".cortex").join("update.json")) .filter(|p| p.exists()); - if let Some(path) = config_path { - if let Ok(content) = std::fs::read_to_string(&path) { - if let Ok(config) = serde_json::from_str(&content) { - return config; + if let Some(path) = config_path + && let Ok(content) = std::fs::read_to_string(&path) + { + match serde_json::from_str(&content) { + Ok(config) => return config, + Err(e) => { + tracing::warn!( + "Failed to parse update config at {}: {}. Using defaults.", + path.display(), + e + ); } } } diff --git a/src/cortex-update/src/download.rs b/src/cortex-update/src/download.rs index 1ec37e1..5d66de7 100644 --- a/src/cortex-update/src/download.rs +++ b/src/cortex-update/src/download.rs @@ -63,16 +63,13 @@ impl Downloader { /// Uses a randomly-named subdirectory to prevent symlink attacks and /// predictable file name exploits. pub fn new(client: CortexSoftwareClient) -> UpdateResult { - // Use a random suffix to prevent predictable temp directory names - // This mitigates symlink attacks where an attacker pre-creates - // files with expected names - let random_suffix: u64 = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_nanos() as u64) - .unwrap_or(0) - ^ std::process::id() as u64; + // Use cryptographically secure random bytes to prevent predictable temp directory names + // This mitigates symlink attacks where an attacker pre-creates files with expected names + let mut random_bytes = [0u8; 8]; + getrandom::getrandom(&mut random_bytes).map_err(|_| UpdateError::TempDirFailed)?; + let random_suffix = u64::from_ne_bytes(random_bytes); - let temp_dir = std::env::temp_dir().join(format!("cortex-update-{:x}", random_suffix)); + let temp_dir = std::env::temp_dir().join(format!("cortex-update-{:016x}", random_suffix)); // Create with restrictive permissions on Unix #[cfg(unix)] @@ -123,6 +120,7 @@ impl Downloader { } /// Download a signature file. + #[allow(dead_code)] pub async fn download_signature(&self, url: &str, version: &str) -> UpdateResult { let filename = format!("{}.sig", version); let dest = self.temp_dir.join(filename); @@ -134,11 +132,13 @@ impl Downloader { } /// Get the temp directory path. + #[allow(dead_code)] pub fn temp_dir(&self) -> &Path { &self.temp_dir } /// Clean up temp files. + #[allow(dead_code)] pub fn cleanup(&self) -> UpdateResult<()> { if self.temp_dir.exists() { std::fs::remove_dir_all(&self.temp_dir)?; diff --git a/src/cortex-update/src/error.rs b/src/cortex-update/src/error.rs index 1e16d9c..c4d157f 100644 --- a/src/cortex-update/src/error.rs +++ b/src/cortex-update/src/error.rs @@ -90,6 +90,10 @@ pub enum UpdateError { // Cancelled #[error("Update cancelled by user")] Cancelled, + + // Security errors + #[error("Insecure URL rejected (HTTPS required): {url}")] + InsecureUrl { url: String }, } impl UpdateError { diff --git a/src/cortex-update/src/install.rs b/src/cortex-update/src/install.rs index a4772fc..d757da9 100644 --- a/src/cortex-update/src/install.rs +++ b/src/cortex-update/src/install.rs @@ -120,37 +120,114 @@ impl Installer { } /// Extract a tar.gz archive. - async fn extract_tar_gz(&self, archive: &Path, dest: &Path) -> UpdateResult { + /// + /// Uses single-pass extraction with inline validation to prevent TOCTOU race conditions. + async fn extract_tar_gz(&self, archive_path: &Path, dest: &Path) -> UpdateResult { use flate2::read::GzDecoder; use std::fs::File; use tar::Archive; - let file = File::open(archive)?; + let file = File::open(archive_path)?; let gz = GzDecoder::new(file); let mut archive = Archive::new(gz); - archive - .unpack(dest) + // Single-pass: validate and extract each entry to prevent TOCTOU race conditions + for entry_result in archive + .entries() .map_err(|e| UpdateError::ExtractionFailed { message: e.to_string(), + })? + { + let mut entry = entry_result.map_err(|e| UpdateError::ExtractionFailed { + message: e.to_string(), + })?; + + let path = entry.path().map_err(|e| UpdateError::ExtractionFailed { + message: e.to_string(), })?; + // Check for path traversal + if path + .components() + .any(|c| matches!(c, std::path::Component::ParentDir)) + { + return Err(UpdateError::ExtractionFailed { + message: format!("Path traversal detected in archive: {}", path.display()), + }); + } + + // Extract the entry immediately after validation + entry + .unpack_in(dest) + .map_err(|e| UpdateError::ExtractionFailed { + message: e.to_string(), + })?; + } + self.find_binary(dest).await } /// Extract a zip archive. - async fn extract_zip(&self, archive: &Path, dest: &Path) -> UpdateResult { - let file = std::fs::File::open(archive)?; + /// + /// Uses single-pass extraction with inline validation to prevent TOCTOU race conditions. + async fn extract_zip(&self, archive_path: &Path, dest: &Path) -> UpdateResult { + let file = std::fs::File::open(archive_path)?; let mut archive = zip::ZipArchive::new(file).map_err(|e| UpdateError::ExtractionFailed { message: e.to_string(), })?; - archive - .extract(dest) - .map_err(|e| UpdateError::ExtractionFailed { - message: e.to_string(), - })?; + // Single-pass: validate and extract each entry to prevent TOCTOU race conditions + for i in 0..archive.len() { + let mut zip_file = archive + .by_index(i) + .map_err(|e| UpdateError::ExtractionFailed { + message: e.to_string(), + })?; + + let path = std::path::Path::new(zip_file.name()); + + // Check for path traversal + if path + .components() + .any(|c| matches!(c, std::path::Component::ParentDir)) + { + return Err(UpdateError::ExtractionFailed { + message: format!("Path traversal detected in archive: {}", zip_file.name()), + }); + } + + // Determine the output path + let outpath = dest.join(zip_file.name()); + + // Extract the entry immediately after validation + if zip_file.is_dir() { + std::fs::create_dir_all(&outpath)?; + } else { + // Ensure parent directory exists + if let Some(parent) = outpath.parent() { + std::fs::create_dir_all(parent)?; + } + let mut outfile = + std::fs::File::create(&outpath).map_err(|e| UpdateError::ExtractionFailed { + message: format!("Failed to create file {}: {}", outpath.display(), e), + })?; + std::io::copy(&mut zip_file, &mut outfile).map_err(|e| { + UpdateError::ExtractionFailed { + message: format!("Failed to extract {}: {}", outpath.display(), e), + } + })?; + + // Set file permissions on Unix + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + if let Some(mode) = zip_file.unix_mode() { + std::fs::set_permissions(&outpath, std::fs::Permissions::from_mode(mode))?; + } + } + } + } self.find_binary(dest).await } @@ -244,6 +321,11 @@ impl Installer { const MOVEFILE_REPLACE_EXISTING: u32 = 0x1; const MOVEFILE_DELAY_UNTIL_REBOOT: u32 = 0x4; + // SAFETY: MoveFileExW is a Windows API function that schedules a file move/rename + // operation to occur at the next system restart. We pass valid null-terminated + // wide strings for source and destination paths. The MOVEFILE_DELAY_UNTIL_REBOOT + // flag ensures this is a deferred operation. The return value of 0 indicates failure, + // in which case we retrieve the error via last_os_error(). let result = unsafe { windows_sys::Win32::Storage::FileSystem::MoveFileExW( old_path.as_ptr(), @@ -269,6 +351,7 @@ impl Installer { } /// Check if we have permission to write to the installation directory. +#[allow(dead_code)] pub fn check_write_permission() -> UpdateResult<()> { let exe_path = std::env::current_exe()?; let parent = exe_path diff --git a/src/cortex-update/src/lib.rs b/src/cortex-update/src/lib.rs index f3cdc1b..ab99a06 100644 --- a/src/cortex-update/src/lib.rs +++ b/src/cortex-update/src/lib.rs @@ -1,4 +1,3 @@ -#![allow(warnings, clippy::all)] //! Cortex Update - Auto-update system for Cortex CLI //! //! Provides automatic update checking and installation via: @@ -36,6 +35,7 @@ mod version; pub use api::{CortexSoftwareClient, ReleaseAsset, ReleaseInfo}; pub use config::{ReleaseChannel, UpdateConfig, UpdateMode}; +pub use download::DownloadProgress; pub use error::{UpdateError, UpdateResult}; pub use install::DownloadedUpdate; pub use manager::{UpdateInfo, UpdateManager, UpdateOutcome}; diff --git a/src/cortex-update/src/manager.rs b/src/cortex-update/src/manager.rs index 16b1525..9b7f2a0 100644 --- a/src/cortex-update/src/manager.rs +++ b/src/cortex-update/src/manager.rs @@ -1,7 +1,5 @@ //! Update manager - main API for update operations. -use std::path::PathBuf; - use crate::CURRENT_VERSION; use crate::api::{CortexSoftwareClient, ReleaseAsset, ReleaseInfo}; use crate::config::{ReleaseChannel, UpdateConfig}; @@ -61,7 +59,7 @@ impl UpdateManager { /// Create with a specific config. pub fn with_config(config: UpdateConfig) -> UpdateResult { let client = if let Some(url) = &config.custom_url { - CortexSoftwareClient::with_url(url.clone()) + CortexSoftwareClient::with_url(url.clone())? } else { CortexSoftwareClient::new() }; @@ -89,10 +87,12 @@ impl UpdateManager { pub async fn check_update(&self) -> UpdateResult> { // Try to use cache first if let Some(cache) = VersionCache::load() { - if cache.is_valid(&self.config) { - if cache.has_update() && !self.config.is_version_skipped(&cache.latest.version) { - return Ok(Some(self.build_update_info(&cache.latest)?)); - } + if cache.is_valid(&self.config) + && cache.has_update() + && !self.config.is_version_skipped(&cache.latest.version) + { + return Ok(Some(self.build_update_info(&cache.latest)?)); + } else if cache.is_valid(&self.config) { return Ok(None); } } diff --git a/src/cortex-update/src/method.rs b/src/cortex-update/src/method.rs index 6a81c2b..2bdae59 100644 --- a/src/cortex-update/src/method.rs +++ b/src/cortex-update/src/method.rs @@ -1,6 +1,8 @@ //! Installation method detection. use serde::{Deserialize, Serialize}; +use std::path::Path; +#[cfg(test)] use std::path::PathBuf; /// Installation method for Cortex CLI. @@ -27,10 +29,10 @@ impl InstallMethod { /// Detect the installation method based on environment and paths. pub fn detect() -> Self { // 1. Check executable path first - if let Ok(exe_path) = std::env::current_exe() { - if let Some(method) = Self::detect_from_path(&exe_path) { - return method; - } + if let Ok(exe_path) = std::env::current_exe() + && let Some(method) = Self::detect_from_path(&exe_path) + { + return method; } // 2. Platform-specific defaults @@ -40,7 +42,7 @@ impl InstallMethod { if which_exists("winget") { return Self::WinGet; } - return Self::PowerShellScript; + Self::PowerShellScript } #[cfg(not(windows))] @@ -49,12 +51,12 @@ impl InstallMethod { if which_exists("brew") { return Self::Homebrew; } - return Self::CurlScript; + Self::CurlScript } } /// Detect from executable path. - fn detect_from_path(path: &PathBuf) -> Option { + fn detect_from_path(path: &Path) -> Option { let path_str = path.to_string_lossy().to_lowercase(); // Homebrew paths (macOS and Linux) diff --git a/src/cortex-update/src/verify.rs b/src/cortex-update/src/verify.rs index b338c82..3fbac30 100644 --- a/src/cortex-update/src/verify.rs +++ b/src/cortex-update/src/verify.rs @@ -34,6 +34,7 @@ pub async fn verify_sha256(path: &Path, expected: &str) -> UpdateResult<()> { } /// Verify SHA256 checksum synchronously (for smaller files). +#[allow(dead_code)] pub fn verify_sha256_sync(path: &Path, expected: &str) -> UpdateResult<()> { let content = std::fs::read(path)?; let result = Sha256::digest(&content); @@ -49,6 +50,7 @@ pub fn verify_sha256_sync(path: &Path, expected: &str) -> UpdateResult<()> { } /// Calculate SHA256 hash of a file. +#[allow(dead_code)] pub async fn calculate_sha256(path: &Path) -> UpdateResult { let mut file = tokio::fs::File::open(path).await?; let mut hasher = Sha256::new(); diff --git a/src/cortex-update/src/version.rs b/src/cortex-update/src/version.rs index c8f81d8..d174593 100644 --- a/src/cortex-update/src/version.rs +++ b/src/cortex-update/src/version.rs @@ -125,6 +125,7 @@ fn parse_version(version: &str) -> (u32, u32, u32, String) { } /// Check if a version meets the minimum requirement. +#[allow(dead_code)] pub fn meets_minimum(current: &str, minimum: &str) -> bool { compare_versions(current, minimum) != VersionComparison::Older } From aea4490ce2343093d6b3ddd647e14c78a7952c1b Mon Sep 17 00:00:00 2001 From: echobt Date: Thu, 5 Feb 2026 16:42:59 +0000 Subject: [PATCH 2/2] fix(tui): remove unused UpdateStatus enum variants Removed unused variants (Checking, CheckFailed, UpToDate) from UpdateStatus enum as per Greptile code review recommendation. Only variants that are actually used in the codebase are retained: NotChecked, Available, Downloading, and ReadyToRestart. --- src/cortex-tui/src/app/state.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/cortex-tui/src/app/state.rs b/src/cortex-tui/src/app/state.rs index f60659d..9177822 100644 --- a/src/cortex-tui/src/app/state.rs +++ b/src/cortex-tui/src/app/state.rs @@ -31,8 +31,6 @@ pub enum UpdateStatus { /// No update check performed yet #[default] NotChecked, - /// Currently checking for updates - Checking, /// An update is available Available { /// The new version available @@ -50,13 +48,6 @@ pub enum UpdateStatus { /// The version that was downloaded version: String, }, - /// Update check failed (network error, etc.) - CheckFailed { - /// Error message - error: String, - }, - /// Already on the latest version - UpToDate, } impl UpdateStatus {