monothink 0.5.2

A totally non-agentic coding assistant
/*
 * This file is part of monothink.
 *
 * monothink is free software: you can redistribute it and/or modify it under the terms
 * of the GNU General Public License as published by the Free Software Foundation,
 * either version 3 of the License, or (at your option) any later version.
 *
 * monothink is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with monothink.
 * If not, see <https://www.gnu.org/licenses/>.
 */

use anyhow::Result;
use cargo_metadata::MetadataCommand;
use std::{cell::RefCell, collections::HashMap, path::PathBuf};
use tokio::process::Command;

type PackageId = String;

#[derive(Debug, Clone, PartialEq)]
pub enum PackageResolverError {
    PackageNotFound { identifier: String },
    SourceNotAvailable { identifier: String },
}

#[derive(Debug, Clone, PartialEq)]
pub struct Package {
    pub id: PackageId,
    pub name: String,
    pub source: Option<PathBuf>,
    pub version: String,
}

#[derive(Debug, Clone)]
pub struct PackageResolver {
    pub active_pacmans: Vec<String>,
    pub packages: RefCell<HashMap<PackageId, Package>>,
    pub refresh_deps: RefCell<bool>,
    pub workspace_dir: PathBuf,
}

impl PackageResolver {
    pub async fn new(workspace_dir: PathBuf) -> PackageResolver {
        let cargo_success = Command::new("cargo")
            .args(["metadata", "--no-deps", "--format-version", "1"])
            .current_dir(&workspace_dir)
            .output()
            .await
            .map_or_else(
                |err| {
                    // @Todo log error
                    false
                },
                |val| val.status.success(),
            );

        if cargo_success {
            PackageResolver {
                active_pacmans: vec!["cargo".to_string()],
                packages: RefCell::new(HashMap::new()),
                refresh_deps: RefCell::new(true),
                workspace_dir,
            }
        } else {
            PackageResolver {
                active_pacmans: Vec::new(),
                packages: RefCell::new(HashMap::new()),
                refresh_deps: RefCell::new(true),
                workspace_dir,
            }
        }
    }

    // @todo filter by language or pacman
    pub fn package_root(&self, id: Option<&str>) -> Result<Option<String>, PackageResolverError> {
        match id {
            Some(pckg_id) => {
                // allow the llm to explicitly specifiy a workspace root
                if pckg_id.is_empty() || pckg_id == "\"\"" || pckg_id == "workspace" {
                    Ok(None)
                } else {
                    self.package_source(pckg_id).map(Some)
                }
            }
            None => Ok(None),
        }
    }

    pub fn package_source(&self, id: &str) -> Result<String, PackageResolverError> {
        self.refresh_dependencies();
        let packages = self.packages.borrow();
        packages
            .get(id)
            .ok_or(PackageResolverError::PackageNotFound {
                identifier: id.to_string(),
            })
            .and_then(|pckg| match &pckg.source {
                Some(path) => Ok(path.to_string_lossy().to_string()),
                None => Err(PackageResolverError::SourceNotAvailable {
                    identifier: id.to_string(),
                }),
            })
    }

    pub fn list_pacmans(&self) -> Vec<String> {
        self.active_pacmans.clone()
    }
    // @todo dependencies(pacman: String, package: Option<PackageId>) -> Result<Vec<Dependency>>

    // @todo filter by language or pacman
    pub fn search_dependency(&self, pattern: &str) -> Vec<String> {
        self.refresh_dependencies();
        let packages = self.packages.borrow();
        packages
            .iter()
            .filter(|pckg| pckg.1.name.contains(pattern))
            .map(|pckg| pckg.0.to_owned())
            .collect()
    }

    // populate dependencies on first run, update periodically
    fn refresh_dependencies(&self) {
        let mut refresh_deps = self.refresh_deps.borrow_mut();
        let mut packages = self.packages.borrow_mut();
        if *refresh_deps {
            *refresh_deps = false;
            let metadata = MetadataCommand::new()
                .current_dir(&self.workspace_dir)
                .exec()
                .unwrap();
            for package in metadata.packages.iter() {
                packages.insert(
                    format!("{}:{}@{}", "cargo", package.name, package.version),
                    Package {
                        id: package.id.repr.clone(),
                        name: package.name.to_string(),
                        source: package
                            .manifest_path
                            .parent()
                            .map(|p| p.to_path_buf().into_std_path_buf()),
                        version: package.version.to_string(),
                    },
                );
            }
        }
    }
}