feat: update checker — startup prompt + Settings → About button
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 3m2s
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 3m2s
Checks Gitea releases API for latest version on startup. If newer version available, shows confirm dialog to open download page. Also adds "Check for Updates" button in Settings → About with version comparison, release notes display, and download button. Backend: check_for_updates command with semver comparison (6 tests). 96 total tests, zero warnings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e9b504c733
commit
4b26d9190b
@ -10,4 +10,5 @@ pub mod pty_commands;
|
|||||||
pub mod mcp_commands;
|
pub mod mcp_commands;
|
||||||
pub mod scanner_commands;
|
pub mod scanner_commands;
|
||||||
pub mod tools_commands;
|
pub mod tools_commands;
|
||||||
|
pub mod updater;
|
||||||
pub mod tools_commands_r2;
|
pub mod tools_commands_r2;
|
||||||
|
|||||||
94
src-tauri/src/commands/updater.rs
Normal file
94
src-tauri/src/commands/updater.rs
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
//! Version check against Gitea releases API.
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct UpdateInfo {
|
||||||
|
pub current_version: String,
|
||||||
|
pub latest_version: String,
|
||||||
|
pub update_available: bool,
|
||||||
|
pub download_url: String,
|
||||||
|
pub release_notes: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check Gitea for the latest release and compare with current version.
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn check_for_updates() -> Result<UpdateInfo, String> {
|
||||||
|
let current = env!("CARGO_PKG_VERSION").to_string();
|
||||||
|
|
||||||
|
let client = reqwest::Client::builder()
|
||||||
|
.timeout(std::time::Duration::from_secs(10))
|
||||||
|
.build()
|
||||||
|
.map_err(|e| format!("HTTP client error: {}", e))?;
|
||||||
|
|
||||||
|
let resp = client
|
||||||
|
.get("https://git.command.vigilcyber.com/api/v1/repos/vstockwell/wraith/releases?limit=1")
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to check for updates: {}", e))?;
|
||||||
|
|
||||||
|
let releases: Vec<serde_json::Value> = resp.json().await
|
||||||
|
.map_err(|e| format!("Failed to parse releases: {}", e))?;
|
||||||
|
|
||||||
|
let latest = releases.first()
|
||||||
|
.ok_or_else(|| "No releases found".to_string())?;
|
||||||
|
|
||||||
|
let tag = latest.get("tag_name")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("v0.0.0")
|
||||||
|
.trim_start_matches('v')
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let notes = latest.get("body")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let html_url = latest.get("html_url")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("https://git.command.vigilcyber.com/vstockwell/wraith/releases")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let update_available = version_is_newer(&tag, ¤t);
|
||||||
|
|
||||||
|
Ok(UpdateInfo {
|
||||||
|
current_version: current,
|
||||||
|
latest_version: tag,
|
||||||
|
update_available,
|
||||||
|
download_url: html_url,
|
||||||
|
release_notes: notes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compare semver strings. Returns true if `latest` is newer than `current`.
|
||||||
|
fn version_is_newer(latest: &str, current: &str) -> bool {
|
||||||
|
let parse = |v: &str| -> Vec<u32> {
|
||||||
|
v.split('.').filter_map(|s| s.parse().ok()).collect()
|
||||||
|
};
|
||||||
|
let l = parse(latest);
|
||||||
|
let c = parse(current);
|
||||||
|
for i in 0..3 {
|
||||||
|
let lv = l.get(i).copied().unwrap_or(0);
|
||||||
|
let cv = c.get(i).copied().unwrap_or(0);
|
||||||
|
if lv > cv { return true; }
|
||||||
|
if lv < cv { return false; }
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn version_comparison() {
|
||||||
|
assert!(version_is_newer("1.5.7", "1.5.6"));
|
||||||
|
assert!(version_is_newer("1.6.0", "1.5.9"));
|
||||||
|
assert!(version_is_newer("2.0.0", "1.9.9"));
|
||||||
|
assert!(!version_is_newer("1.5.6", "1.5.6"));
|
||||||
|
assert!(!version_is_newer("1.5.5", "1.5.6"));
|
||||||
|
assert!(!version_is_newer("1.4.0", "1.5.0"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -173,6 +173,7 @@ pub fn run() {
|
|||||||
commands::scanner_commands::scan_network, commands::scanner_commands::scan_ports, commands::scanner_commands::quick_scan,
|
commands::scanner_commands::scan_network, commands::scanner_commands::scan_ports, commands::scanner_commands::quick_scan,
|
||||||
commands::tools_commands::tool_ping, commands::tools_commands::tool_traceroute, commands::tools_commands::tool_wake_on_lan, commands::tools_commands::tool_generate_ssh_key, commands::tools_commands::tool_generate_password,
|
commands::tools_commands::tool_ping, commands::tools_commands::tool_traceroute, commands::tools_commands::tool_wake_on_lan, commands::tools_commands::tool_generate_ssh_key, commands::tools_commands::tool_generate_password,
|
||||||
commands::tools_commands_r2::tool_dns_lookup, commands::tools_commands_r2::tool_whois, commands::tools_commands_r2::tool_bandwidth_iperf, commands::tools_commands_r2::tool_bandwidth_speedtest, commands::tools_commands_r2::tool_subnet_calc,
|
commands::tools_commands_r2::tool_dns_lookup, commands::tools_commands_r2::tool_whois, commands::tools_commands_r2::tool_bandwidth_iperf, commands::tools_commands_r2::tool_bandwidth_speedtest, commands::tools_commands_r2::tool_subnet_calc,
|
||||||
|
commands::updater::check_for_updates,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
@ -233,6 +233,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Update check -->
|
||||||
|
<div class="pt-2">
|
||||||
|
<button
|
||||||
|
class="w-full px-3 py-2 text-xs font-bold rounded bg-[var(--wraith-accent-blue)] text-black cursor-pointer disabled:opacity-40"
|
||||||
|
:disabled="updateChecking"
|
||||||
|
@click="checkUpdates"
|
||||||
|
>
|
||||||
|
{{ updateChecking ? "Checking..." : "Check for Updates" }}
|
||||||
|
</button>
|
||||||
|
<div v-if="updateInfo" class="mt-2 p-3 rounded bg-[#0d1117] border border-[#30363d]">
|
||||||
|
<template v-if="updateInfo.updateAvailable">
|
||||||
|
<p class="text-xs text-[#3fb950] mb-1">Update available: v{{ updateInfo.latestVersion }}</p>
|
||||||
|
<p v-if="updateInfo.releaseNotes" class="text-[10px] text-[var(--wraith-text-muted)] mb-2 max-h-20 overflow-auto">{{ updateInfo.releaseNotes }}</p>
|
||||||
|
<button
|
||||||
|
class="w-full px-3 py-1.5 text-xs font-bold rounded bg-[#238636] text-white cursor-pointer"
|
||||||
|
@click="downloadUpdate"
|
||||||
|
>
|
||||||
|
Download v{{ updateInfo.latestVersion }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<p v-else class="text-xs text-[var(--wraith-text-muted)]">You're on the latest version.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2 pt-2">
|
<div class="flex gap-2 pt-2">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
@ -281,6 +305,36 @@ interface CopilotPreset { name: string; shell: string; command: string; }
|
|||||||
const visible = ref(false);
|
const visible = ref(false);
|
||||||
const activeSection = ref<Section>("general");
|
const activeSection = ref<Section>("general");
|
||||||
const copilotPresets = ref<CopilotPreset[]>([]);
|
const copilotPresets = ref<CopilotPreset[]>([]);
|
||||||
|
|
||||||
|
interface UpdateCheckInfo {
|
||||||
|
currentVersion: string;
|
||||||
|
latestVersion: string;
|
||||||
|
updateAvailable: boolean;
|
||||||
|
downloadUrl: string;
|
||||||
|
releaseNotes: string;
|
||||||
|
}
|
||||||
|
const updateChecking = ref(false);
|
||||||
|
const updateInfo = ref<UpdateCheckInfo | null>(null);
|
||||||
|
|
||||||
|
async function checkUpdates(): Promise<void> {
|
||||||
|
updateChecking.value = true;
|
||||||
|
updateInfo.value = null;
|
||||||
|
try {
|
||||||
|
updateInfo.value = await invoke<UpdateCheckInfo>("check_for_updates");
|
||||||
|
} catch (err) {
|
||||||
|
alert(`Update check failed: ${err}`);
|
||||||
|
}
|
||||||
|
updateChecking.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadUpdate(): Promise<void> {
|
||||||
|
if (!updateInfo.value?.downloadUrl) return;
|
||||||
|
try {
|
||||||
|
await shellOpen(updateInfo.value.downloadUrl);
|
||||||
|
} catch {
|
||||||
|
window.open(updateInfo.value.downloadUrl, "_blank");
|
||||||
|
}
|
||||||
|
}
|
||||||
const currentVersion = ref("loading...");
|
const currentVersion = ref("loading...");
|
||||||
|
|
||||||
const sections = [
|
const sections = [
|
||||||
|
|||||||
@ -429,6 +429,17 @@ function handleKeydown(event: KeyboardEvent): void {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
document.addEventListener("keydown", handleKeydown);
|
document.addEventListener("keydown", handleKeydown);
|
||||||
await connectionStore.loadAll();
|
await connectionStore.loadAll();
|
||||||
|
|
||||||
|
// Check for updates on startup (non-blocking)
|
||||||
|
invoke<{ currentVersion: string; latestVersion: string; updateAvailable: boolean; downloadUrl: string }>("check_for_updates")
|
||||||
|
.then((info) => {
|
||||||
|
if (info.updateAvailable) {
|
||||||
|
if (confirm(`Wraith v${info.latestVersion} is available (you have v${info.currentVersion}). Open download page?`)) {
|
||||||
|
import("@tauri-apps/plugin-shell").then(({ open }) => open(info.downloadUrl)).catch(() => window.open(info.downloadUrl, "_blank"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {}); // Silent fail — no internet is fine
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user