frigg 0.4.2

Local-first MCP server for code understanding.
Documentation
use super::*;

#[cfg(test)]
static TEST_PRECISE_GENERATOR_BIN_OVERRIDE: OnceLock<Mutex<Option<PathBuf>>> = OnceLock::new();

impl FriggMcpServer {
    pub(in crate::mcp::server) fn load_workspace_precise_config(
        root: &Path,
    ) -> WorkspacePreciseConfig {
        let config_path = root.join(PRECISE_WORKSPACE_CONFIG_FILE);
        let Ok(raw) = fs::read_to_string(&config_path) else {
            return WorkspacePreciseConfig::default();
        };

        match serde_json::from_str::<WorkspacePreciseConfigFile>(&raw) {
            Ok(config) => config.precise,
            Err(error) => {
                warn!(
                    path = %config_path.display(),
                    error = %error,
                    "failed to parse workspace precise config; falling back to defaults"
                );
                WorkspacePreciseConfig::default()
            }
        }
    }

    pub(in crate::mcp::server) fn workspace_precise_generator_disabled(
        config: &WorkspacePreciseConfig,
        generator_id: &str,
    ) -> bool {
        config
            .disabled_generators
            .iter()
            .any(|value| value.eq_ignore_ascii_case(generator_id))
    }

    pub(in crate::mcp::server) fn workspace_precise_generator_extra_args(
        config: &WorkspacePreciseConfig,
        generator_id: &str,
    ) -> Vec<String> {
        config
            .generator_extra_args
            .iter()
            .find(|(key, _)| key.eq_ignore_ascii_case(generator_id))
            .map(|(_, args)| args.clone())
            .unwrap_or_default()
    }

    pub(in crate::mcp::server) fn compile_workspace_precise_exclude_matcher(
        root: &Path,
        patterns: &[String],
    ) -> Option<Gitignore> {
        if patterns.is_empty() {
            return None;
        }

        let mut builder = GitignoreBuilder::new(root);
        for pattern in patterns {
            if let Err(error) = builder.add_line(None, pattern) {
                warn!(
                    root = %root.display(),
                    pattern,
                    error = %error,
                    "failed to parse workspace precise exclude pattern"
                );
            }
        }

        Some(builder.build().unwrap_or_else(|error| {
            warn!(
                root = %root.display(),
                error = %error,
                "failed to compile workspace precise exclude matcher"
            );
            Gitignore::empty()
        }))
    }

    pub(in crate::mcp::server) fn workspace_precise_excludes_path(
        root: &Path,
        path: &Path,
        matcher: Option<&Gitignore>,
        is_dir: bool,
    ) -> bool {
        let Some(matcher) = matcher else {
            return false;
        };
        let Ok(relative) = path.strip_prefix(root) else {
            return false;
        };
        if relative.as_os_str().is_empty() {
            return false;
        }
        matcher
            .matched_path_or_any_parents(relative, is_dir)
            .is_ignore()
    }

    pub(in crate::mcp::server) fn create_precise_generation_workspace(
        root: &Path,
        matcher: &Gitignore,
        generator_id: &str,
    ) -> Result<PathBuf, String> {
        let staging_root = root
            .join(".frigg")
            .join("tmp")
            .join("precise-generation")
            .join(format!("{generator_id}-{}", Self::scip_now_unix_ms()));
        fs::create_dir_all(&staging_root).map_err(|error| {
            format!(
                "failed to prepare filtered precise generation workspace {}: {error}",
                staging_root.display()
            )
        })?;

        let walker = WalkDir::new(root)
            .follow_links(false)
            .into_iter()
            .filter_entry(|entry| {
                let path = entry.path();
                if path == root {
                    return true;
                }
                let Ok(relative) = path.strip_prefix(root) else {
                    return false;
                };
                if relative
                    .components()
                    .next()
                    .is_some_and(|component| component.as_os_str() == ".frigg")
                {
                    return false;
                }
                !matcher
                    .matched_path_or_any_parents(relative, entry.file_type().is_dir())
                    .is_ignore()
            });

        for entry in walker {
            let entry = entry.map_err(|error| {
                format!(
                    "failed to walk filtered precise generation workspace {}: {error}",
                    root.display()
                )
            })?;
            let path = entry.path();
            if path == root {
                continue;
            }
            let relative = path.strip_prefix(root).map_err(|error| {
                format!(
                    "failed to resolve filtered precise generation relative path for {}: {error}",
                    path.display()
                )
            })?;
            let target = staging_root.join(relative);
            if entry.file_type().is_dir() {
                fs::create_dir_all(&target).map_err(|error| {
                    format!(
                        "failed to create filtered precise generation directory {}: {error}",
                        target.display()
                    )
                })?;
                continue;
            }
            if let Some(parent) = target.parent() {
                fs::create_dir_all(parent).map_err(|error| {
                    format!(
                        "failed to prepare filtered precise generation parent {}: {error}",
                        parent.display()
                    )
                })?;
            }
            Self::link_precise_generation_entry(path, &target).map_err(|error| {
                format!(
                    "failed to link filtered precise generation entry {} -> {}: {error}",
                    path.display(),
                    target.display()
                )
            })?;
        }

        Ok(staging_root)
    }

    #[cfg(unix)]
    fn link_precise_generation_entry(source: &Path, target: &Path) -> std::io::Result<()> {
        std::os::unix::fs::symlink(source, target)
    }

    #[cfg(not(unix))]
    fn link_precise_generation_entry(source: &Path, target: &Path) -> std::io::Result<()> {
        fs::copy(source, target).map(|_| ())
    }

    #[cfg(test)]
    pub(in crate::mcp::server) fn set_test_precise_generator_bin_override(
        bin_dir: Option<PathBuf>,
    ) {
        *TEST_PRECISE_GENERATOR_BIN_OVERRIDE
            .get_or_init(|| Mutex::new(None))
            .lock()
            .expect("test precise generator override lock should not be poisoned") = bin_dir;
    }

    #[cfg(test)]
    pub(in crate::mcp::server) fn test_precise_generator_bin_override() -> Option<PathBuf> {
        TEST_PRECISE_GENERATOR_BIN_OVERRIDE
            .get_or_init(|| Mutex::new(None))
            .lock()
            .expect("test precise generator override lock should not be poisoned")
            .clone()
    }
}