frigg 0.3.2

Local-first MCP server for code understanding.
Documentation
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};

use crate::domain::model::stable_repository_id_for_root;
use crate::storage::resolve_provenance_db_path;

#[derive(Debug, Clone)]
pub(crate) struct AttachedWorkspace {
    pub repository_id: String,
    pub runtime_repository_id: String,
    pub display_name: String,
    pub root: PathBuf,
    pub db_path: PathBuf,
}

#[derive(Debug, Clone, Default)]
pub(crate) struct WorkspaceRegistry {
    workspaces: Vec<AttachedWorkspace>,
    by_canonical_root: BTreeMap<PathBuf, usize>,
    active_session_counts: BTreeMap<String, usize>,
}

impl WorkspaceRegistry {
    pub(crate) fn from_startup_repositories<I>(repositories: I) -> Self
    where
        I: IntoIterator<Item = (String, String, String)>,
    {
        let mut registry = Self::default();
        for (runtime_repository_id, display_name, root_path) in repositories {
            let root = PathBuf::from(&root_path)
                .canonicalize()
                .unwrap_or_else(|_| PathBuf::from(&root_path));
            let repository_id = stable_repository_id_for_root(&root).0;
            registry.insert_with_repository_id(
                root,
                repository_id,
                runtime_repository_id,
                display_name,
            );
        }
        registry
    }

    pub(crate) fn known_workspaces(&self) -> Vec<AttachedWorkspace> {
        self.workspaces.clone()
    }

    pub(crate) fn workspace_by_repository_id(
        &self,
        repository_id: &str,
    ) -> Option<AttachedWorkspace> {
        self.workspace_by_any_repository_id(repository_id)
    }

    pub(crate) fn workspace_by_any_repository_id(
        &self,
        repository_id: &str,
    ) -> Option<AttachedWorkspace> {
        self.workspaces
            .iter()
            .find(|workspace| {
                workspace.repository_id == repository_id
                    || workspace.runtime_repository_id == repository_id
            })
            .cloned()
    }

    pub(crate) fn insert_with_repository_id(
        &mut self,
        canonical_root: PathBuf,
        repository_id: String,
        runtime_repository_id: String,
        display_name: String,
    ) -> AttachedWorkspace {
        if let Some(index) = self.by_canonical_root.get(&canonical_root).copied() {
            return self.workspaces[index].clone();
        }

        let workspace = AttachedWorkspace {
            db_path: storage_db_path_for_root(&canonical_root),
            repository_id,
            runtime_repository_id,
            display_name,
            root: canonical_root.clone(),
        };
        self.by_canonical_root
            .insert(canonical_root, self.workspaces.len());
        self.workspaces.push(workspace.clone());
        workspace
    }

    pub(crate) fn get_or_insert(&mut self, canonical_root: PathBuf) -> AttachedWorkspace {
        let display_name = display_name_for_root(&canonical_root);
        let repository_id = stable_repository_id_for_root(&canonical_root).0;
        self.insert_with_repository_id(
            canonical_root,
            repository_id.clone(),
            repository_id,
            display_name,
        )
    }

    pub(crate) fn mark_session_adopted(&mut self, repository_id: &str) -> usize {
        let count = self
            .active_session_counts
            .entry(repository_id.to_owned())
            .or_insert(0);
        *count = count.saturating_add(1);
        *count
    }

    pub(crate) fn mark_session_released(&mut self, repository_id: &str) -> usize {
        let Some(count) = self.active_session_counts.get_mut(repository_id) else {
            return 0;
        };
        *count = count.saturating_sub(1);
        let remaining = *count;
        if remaining == 0 {
            self.active_session_counts.remove(repository_id);
        }
        remaining
    }

    pub(crate) fn active_session_count(&self, repository_id: &str) -> usize {
        self.active_session_counts
            .get(repository_id)
            .copied()
            .unwrap_or(0)
    }
}

fn display_name_for_root(root: &Path) -> String {
    root.file_name()
        .and_then(|name| name.to_str())
        .map(ToOwned::to_owned)
        .unwrap_or_else(|| root.display().to_string())
}

fn storage_db_path_for_root(root: &Path) -> PathBuf {
    resolve_provenance_db_path(root).unwrap_or_else(|_| {
        root.join(crate::storage::PROVENANCE_STORAGE_DIR)
            .join(crate::storage::PROVENANCE_STORAGE_DB_FILE)
    })
}