use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use crate::adapters::claude::CLAUDE_ADAPTER_VERSION;
use crate::adapters::gemini::GEMINI_ADAPTER_VERSION;
#[derive(Debug, Serialize, Deserialize)]
pub struct VersionManifest {
pub patina: String,
pub components: HashMap<String, ComponentInfo>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ComponentInfo {
pub version: String,
pub description: String,
}
impl Default for VersionManifest {
fn default() -> Self {
Self::new()
}
}
impl VersionManifest {
pub fn new() -> Self {
let mut components = HashMap::new();
components.insert(
"claude-adapter".to_string(),
ComponentInfo {
version: CLAUDE_ADAPTER_VERSION.to_string(),
description: "Claude AI session management and context generation".to_string(),
},
);
components.insert(
"gemini-adapter".to_string(),
ComponentInfo {
version: GEMINI_ADAPTER_VERSION.to_string(),
description: "Gemini AI context file generation".to_string(),
},
);
Self {
patina: env!("CARGO_PKG_VERSION").to_string(),
components,
}
}
pub fn load(project_path: &Path) -> Result<Self> {
let manifest_path = project_path.join(".patina").join("versions.json");
if manifest_path.exists() {
let content = fs::read_to_string(&manifest_path)?;
Ok(serde_json::from_str(&content)?)
} else {
Ok(Self::new())
}
}
pub fn save(&self, project_path: &Path) -> Result<()> {
let patina_dir = project_path.join(".patina");
fs::create_dir_all(&patina_dir)?;
let manifest_path = patina_dir.join("versions.json");
let content = serde_json::to_string_pretty(self)?;
fs::write(manifest_path, content)?;
Ok(())
}
pub fn get_component_version(&self, component: &str) -> Option<&str> {
self.components.get(component).map(|c| c.version.as_str())
}
pub fn update_component_version(&mut self, component: &str, version: &str) {
if let Some(info) = self.components.get_mut(component) {
info.version = version.to_string();
}
}
}
pub struct UpdateChecker;
impl UpdateChecker {
pub fn get_available_versions() -> HashMap<String, String> {
let mut available = HashMap::new();
available.insert(
"claude-adapter".to_string(),
CLAUDE_ADAPTER_VERSION.to_string(),
);
available.insert(
"gemini-adapter".to_string(),
GEMINI_ADAPTER_VERSION.to_string(),
);
available
}
pub fn check_for_updates(manifest: &VersionManifest) -> Vec<(String, String, String)> {
let available = Self::get_available_versions();
let mut updates = Vec::new();
for (component, available_version) in available {
if let Some(current_version) = manifest.get_component_version(&component) {
if current_version != available_version {
updates.push((component, current_version.to_string(), available_version));
}
}
}
updates
}
pub fn force_all_updates(manifest: &VersionManifest) -> Vec<(String, String, String)> {
let available = Self::get_available_versions();
let mut updates = Vec::new();
for (component, available_version) in available {
let current_version = manifest
.get_component_version(&component)
.unwrap_or(&available_version)
.to_string();
updates.push((component, current_version, available_version));
}
updates
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_version_manifest_new() {
let manifest = VersionManifest::new();
assert_eq!(manifest.patina, env!("CARGO_PKG_VERSION"));
assert!(manifest.components.contains_key("claude-adapter"));
assert!(manifest.components.contains_key("gemini-adapter"));
let claude = &manifest.components["claude-adapter"];
assert_eq!(claude.version, CLAUDE_ADAPTER_VERSION);
assert!(!claude.description.is_empty());
}
#[test]
fn test_version_manifest_default() {
let manifest1 = VersionManifest::default();
let manifest2 = VersionManifest::new();
assert_eq!(manifest1.patina, manifest2.patina);
assert_eq!(manifest1.components.len(), manifest2.components.len());
}
#[test]
fn test_load_nonexistent_manifest() {
let temp_dir = TempDir::new().unwrap();
let manifest = VersionManifest::load(temp_dir.path()).unwrap();
assert_eq!(manifest.patina, env!("CARGO_PKG_VERSION"));
assert_eq!(manifest.components.len(), 2);
}
#[test]
fn test_save_and_load_manifest() {
let temp_dir = TempDir::new().unwrap();
let manifest = VersionManifest::new();
manifest.save(temp_dir.path()).unwrap();
let manifest_path = temp_dir.path().join(".patina").join("versions.json");
assert!(manifest_path.exists());
let loaded = VersionManifest::load(temp_dir.path()).unwrap();
assert_eq!(loaded.patina, manifest.patina);
assert_eq!(loaded.components.len(), manifest.components.len());
}
#[test]
fn test_get_component_version() {
let manifest = VersionManifest::new();
let version = manifest.get_component_version("claude-adapter").unwrap();
assert_eq!(version, CLAUDE_ADAPTER_VERSION);
let version = manifest.get_component_version("nonexistent");
assert!(version.is_none());
}
#[test]
fn test_update_component_version() {
let mut manifest = VersionManifest::new();
manifest.update_component_version("claude-adapter", "2.0.0");
assert_eq!(manifest.components["claude-adapter"].version, "2.0.0");
manifest.update_component_version("new-component", "1.0.0");
assert!(!manifest.components.contains_key("new-component"));
}
#[test]
fn test_update_checker_get_available_versions() {
let versions = UpdateChecker::get_available_versions();
assert_eq!(versions.len(), 2);
assert!(versions.contains_key("claude-adapter"));
assert!(versions.contains_key("gemini-adapter"));
}
#[test]
fn test_update_checker_check_for_updates() {
let mut manifest = VersionManifest::new();
manifest.update_component_version("claude-adapter", "0.1.0");
let updates = UpdateChecker::check_for_updates(&manifest);
assert_eq!(updates.len(), 1);
let (component, current, available) = &updates[0];
assert_eq!(component, "claude-adapter");
assert_eq!(current, "0.1.0");
assert_eq!(available, CLAUDE_ADAPTER_VERSION);
}
#[test]
fn test_update_checker_no_updates_needed() {
let manifest = VersionManifest::new();
let updates = UpdateChecker::check_for_updates(&manifest);
assert!(updates.is_empty());
}
#[test]
fn test_version_manifest_serialization() {
let manifest = VersionManifest::new();
let json = serde_json::to_string(&manifest).unwrap();
let deserialized: VersionManifest = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.patina, manifest.patina);
assert_eq!(deserialized.components.len(), manifest.components.len());
assert_eq!(
deserialized.components["claude-adapter"].version,
manifest.components["claude-adapter"].version
);
}
#[test]
fn test_update_checker_force_all_updates() {
let manifest = VersionManifest::new();
let updates = UpdateChecker::force_all_updates(&manifest);
assert_eq!(updates.len(), 2);
let components: Vec<String> = updates.iter().map(|(c, _, _)| c.clone()).collect();
assert!(components.contains(&"claude-adapter".to_string()));
assert!(components.contains(&"gemini-adapter".to_string()));
}
}