knowledge 0.11.0

The launcher and updater for the Knowledge.Dev MCP binary
Documentation
use crate::{VERSION, built_info};
use anyhow::{Context, Result};
use chrono::{DateTime, Duration, Utc};
use derive_more::{Deref, DerefMut};
use semver::Version;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use tokio::fs;

#[derive(Debug, Deserialize, Serialize)]
pub struct GlobalConfig {
    pub system: String,
}

impl Default for GlobalConfig {
    fn default() -> Self {
        Self {
            system: built_info::CFG_OS.into(),
        }
    }
}

#[derive(Debug, Deserialize, Serialize)]
pub struct LauncherConfig {
    pub global: GlobalConfig,
    pub launcher: AppState,
    #[serde(default)]
    pub player: AppState,
    #[serde(default)]
    pub workbench: AppState,
}

impl Default for LauncherConfig {
    fn default() -> Self {
        Self {
            global: GlobalConfig::default(),
            launcher: AppState {
                version: Some(VERSION.clone()),
                last_check: None,
            },
            player: AppState {
                version: None,
                last_check: None,
            },
            workbench: AppState {
                version: None,
                last_check: None,
            },
        }
    }
}

#[derive(Debug, Default, Deserialize, Serialize)]
pub struct AppState {
    pub version: Option<Version>,
    pub last_check: Option<DateTime<Utc>>,
}

impl AppState {
    pub fn is_update_required(&self) -> bool {
        self.is_not_exist() || self.is_allowed_to_check()
    }

    pub fn is_not_exist(&self) -> bool {
        self.version.is_none()
    }

    pub fn is_allowed_to_check(&self) -> bool {
        let deadline = Utc::now() - Duration::days(1);
        match &self.last_check {
            None => true,
            Some(last) if last <= &deadline => true,
            Some(_recent) => false,
        }
    }

    pub fn update_check(&mut self) {
        self.last_check = Some(Utc::now());
    }

    pub fn is_outdated(&self, recent_version: Version) -> bool {
        if let Some(current_version) = self.version.as_ref() {
            current_version < &recent_version
        } else {
            true
        }
    }
}

#[derive(Debug, Deref, DerefMut)]
pub struct Cacher {
    cache_dir: PathBuf,
    bin_dir: PathBuf,
    state_path: PathBuf,
    #[deref]
    #[deref_mut]
    state: LauncherConfig,
}

impl Cacher {
    pub async fn create() -> Result<Self> {
        let mut cache_dir = dirs::cache_dir().context("Cache directory is not available")?;
        cache_dir.push("knowledge");

        let mut bin_dir = cache_dir.clone();
        bin_dir.push("bin");

        let mut state_path = cache_dir.clone();
        state_path.push("launcher.toml");

        let state = LauncherConfig::default();
        Ok(Self {
            cache_dir,
            bin_dir,
            state_path,
            state,
        })
    }

    pub async fn initialize(&mut self) -> Result<()> {
        fs::create_dir_all(&self.bin_dir).await?;
        if let Ok(contents) = fs::read_to_string(&self.state_path).await {
            if let Ok(state) = toml::from_str(&contents) {
                self.state = state;
            }
        }
        self.state.launcher.version = Some(VERSION.clone());
        self.write_state().await?;
        Ok(())
    }

    pub fn bin_dir(&self) -> &PathBuf {
        &self.bin_dir
    }

    pub async fn write_state(&mut self) -> Result<()> {
        let contents = toml::to_string(&self.state)?;
        fs::write(&self.state_path, contents).await?;
        Ok(())
    }

    pub async fn remove_cache(self) -> Result<()> {
        fs::remove_dir_all(self.cache_dir).await?;
        Ok(())
    }
}