use std::collections::HashMap;
use std::path::{Path, PathBuf};
use crate::config::HookSource;
fn plugin_cache_dir(alias: &str) -> Result<PathBuf, String> {
use crate::plugin::{PluginCache, PluginConfigManager};
let project_url = PluginConfigManager::project()
.ok()
.and_then(|m| m.get_plugin_by_alias(alias).ok().flatten())
.map(|(url, _)| url);
let global_url = PluginConfigManager::global()
.ok()
.and_then(|m| m.get_plugin_by_alias(alias).ok().flatten())
.map(|(url, _)| url);
let url = project_url.or(global_url).ok_or_else(|| {
format!(
"Plugin '{}' is not configured. Run `linthis plugin add {} <url>` first.",
alias, alias
)
})?;
let cache = PluginCache::new().map_err(|e| e.to_string())?;
let cache_path = cache.url_to_cache_path(&url);
if !cache_path.exists() {
return Err(format!(
"Plugin '{}' is configured but not cached. Run `linthis plugin sync` to download it.",
alias
));
}
Ok(cache_path)
}
fn marketplace_plugin_dir(marketplace_url: &str, plugin_name: &str) -> Result<PathBuf, String> {
use crate::plugin::{fetcher::PluginFetcher, PluginCache};
let cache = PluginCache::new().map_err(|e| e.to_string())?;
let marketplace_cache = cache.url_to_cache_path(marketplace_url);
let fetcher = PluginFetcher::new();
if marketplace_cache.exists() {
fetcher
.update_plugin(marketplace_url, &marketplace_cache, None)
.map_err(|e| format!("Failed to update marketplace '{}': {}", marketplace_url, e))?;
} else {
fetcher
.clone_plugin(marketplace_url, &marketplace_cache, None)
.map_err(|e| format!("Failed to clone marketplace '{}': {}", marketplace_url, e))?;
}
let plugin_dir = marketplace_cache.join(plugin_name);
if !plugin_dir.exists() {
return Err(format!(
"Plugin '{}' not found in marketplace '{}'",
plugin_name, marketplace_url
));
}
Ok(plugin_dir)
}
fn git_clone_temp(
git_url: &str,
git_ref: Option<&str>,
inner_path: &str,
) -> Result<(tempfile::TempDir, PathBuf), String> {
use crate::plugin::fetcher::PluginFetcher;
let tmp =
tempfile::TempDir::new().map_err(|e| format!("Failed to create temp directory: {}", e))?;
let fetcher = PluginFetcher::new();
fetcher
.clone_plugin(git_url, tmp.path(), git_ref)
.map_err(|e| format!("Failed to clone '{}': {}", git_url, e))?;
let resolved = tmp.path().join(inner_path);
if !resolved.exists() {
return Err(format!(
"Path '{}' not found in git repo '{}'",
inner_path, git_url
));
}
Ok((tmp, resolved))
}
fn resolve_marketplace_url(
name: &str,
marketplaces: &HashMap<String, String>,
) -> Result<String, String> {
marketplaces.get(name).cloned().ok_or_else(|| {
format!(
"Marketplace '{}' is not defined in [hook.marketplaces]",
name
)
})
}
pub fn resolve_to_string(
source: &HookSource,
project_root: &Path,
marketplaces: &HashMap<String, String>,
) -> Result<String, String> {
match source {
HookSource::File { file } => {
let path = project_root.join(file);
std::fs::read_to_string(&path)
.map_err(|e| format!("Failed to read local file '{}': {}", path.display(), e))
}
HookSource::Plugin { plugin, file } => {
let cache_dir = plugin_cache_dir(plugin)?;
let path = cache_dir.join(file);
std::fs::read_to_string(&path)
.map_err(|e| format!("File '{}' not found in plugin '{}': {}", file, plugin, e))
}
HookSource::Marketplace {
marketplace,
plugin,
file,
} => {
let mkt_url = resolve_marketplace_url(marketplace, marketplaces)?;
let plugin_dir = marketplace_plugin_dir(&mkt_url, plugin)?;
let path = plugin_dir.join(file);
std::fs::read_to_string(&path).map_err(|e| {
format!(
"File '{}' not found in marketplace plugin '{}/{}': {}",
file, marketplace, plugin, e
)
})
}
HookSource::Url { url } => reqwest::blocking::get(url)
.map_err(|e| format!("HTTP GET '{}' failed: {}", url, e))?
.text()
.map_err(|e| format!("Failed to read HTTP response from '{}': {}", url, e)),
HookSource::Git { git, git_ref, path } => {
let (_tmp, resolved) = git_clone_temp(git, git_ref.as_deref(), path)?;
std::fs::read_to_string(&resolved)
.map_err(|e| format!("Failed to read '{}' from git repo '{}': {}", path, git, e))
}
}
}
pub enum ResolvedDir {
Persistent(PathBuf),
Temporary {
_tmp: tempfile::TempDir,
path: PathBuf,
},
}
impl ResolvedDir {
pub fn path(&self) -> &Path {
match self {
ResolvedDir::Persistent(p) => p,
ResolvedDir::Temporary { path, .. } => path,
}
}
}
pub fn resolve_to_dir(
source: &HookSource,
project_root: &Path,
marketplaces: &HashMap<String, String>,
) -> Result<ResolvedDir, String> {
match source {
HookSource::File { file } => {
let path = project_root.join(file);
if !path.is_dir() {
return Err(format!(
"Expected a directory at '{}' but it does not exist or is a file",
path.display()
));
}
Ok(ResolvedDir::Persistent(path))
}
HookSource::Plugin { plugin, file } => {
let cache_dir = plugin_cache_dir(plugin)?;
let path = cache_dir.join(file);
if !path.is_dir() {
return Err(format!(
"Directory '{}' not found in plugin '{}'",
file, plugin
));
}
Ok(ResolvedDir::Persistent(path))
}
HookSource::Marketplace {
marketplace,
plugin,
file,
} => {
let mkt_url = resolve_marketplace_url(marketplace, marketplaces)?;
let plugin_dir = marketplace_plugin_dir(&mkt_url, plugin)?;
let path = plugin_dir.join(file);
if !path.is_dir() {
return Err(format!(
"Directory '{}' not found in marketplace plugin '{}/{}'",
file, marketplace, plugin
));
}
Ok(ResolvedDir::Persistent(path))
}
HookSource::Url { url } => Err(format!(
"The `url` source variant does not support directory resolution (source: {})",
url
)),
HookSource::Git { git, git_ref, path } => {
let (tmp, resolved) = git_clone_temp(git, git_ref.as_deref(), path)?;
if !resolved.is_dir() {
return Err(format!(
"'{}' in git repo '{}' is not a directory",
path, git
));
}
Ok(ResolvedDir::Temporary {
_tmp: tmp,
path: resolved,
})
}
}
}
pub fn fixed_git_hook_path(
project_root: &Path,
tool_type_dir: &str,
event_filename: &str,
) -> Option<PathBuf> {
let path = project_root
.join("hooks")
.join(tool_type_dir)
.join(event_filename);
if path.is_file() {
Some(path)
} else {
None
}
}
pub fn fixed_agent_plugin_dir(
project_root: &Path,
provider_name: &str,
plugin_id: &str,
) -> Option<PathBuf> {
let provider_path = project_root
.join("hooks")
.join("agent")
.join("plugins")
.join(provider_name)
.join(plugin_id);
if provider_path.is_dir() {
return Some(provider_path);
}
let default_path = project_root
.join("hooks")
.join("agent")
.join("plugins")
.join("_default")
.join(plugin_id);
if default_path.is_dir() {
Some(default_path)
} else {
None
}
}