use std::path::{Path, PathBuf};
use tokio::fs;
use super::{PluginError, PluginManifest, PluginResult, PluginRuntime};
#[derive(Debug, Clone)]
pub enum PluginSource {
Local(PathBuf),
Git(String),
Http(String),
Marketplace(String),
}
pub struct PluginLoader {
plugins_dir: PathBuf,
runtime: PluginRuntime,
}
impl PluginLoader {
pub fn new(plugins_dir: PathBuf, runtime: PluginRuntime) -> Self {
Self { plugins_dir, runtime }
}
pub async fn install_plugin(&self, source: PluginSource, name: Option<String>) -> PluginResult<()> {
let plugin_dir = match source {
PluginSource::Local(path) => self.install_from_local(path).await?,
PluginSource::Git(url) => self.install_from_git(&url, name.as_deref()).await?,
PluginSource::Http(url) => self.install_from_http(&url, name.as_deref()).await?,
PluginSource::Marketplace(id) => self.install_from_marketplace(&id, name.as_deref()).await?,
};
self.runtime.load_plugin(&plugin_dir).await?;
Ok(())
}
async fn install_from_local(&self, source_path: PathBuf) -> PluginResult<PathBuf> {
if !source_path.exists() {
return Err(PluginError::LoadingError(format!("Source path does not exist: {}", source_path.display())));
}
let manifest_path = source_path.join(".vtcode-plugin/plugin.json");
if !manifest_path.exists() {
return Err(PluginError::ManifestValidationError(format!(
"Plugin manifest not found in source: {}",
manifest_path.display()
)));
}
let manifest_content = fs::read_to_string(&manifest_path)
.await
.map_err(|e| PluginError::LoadingError(format!("Failed to read manifest: {e}")))?;
let manifest: PluginManifest = serde_json::from_str(&manifest_content)
.map_err(|e| PluginError::ManifestValidationError(format!("Invalid manifest JSON: {e}")))?;
let install_dir = self.plugins_dir.join(&manifest.name);
fs::create_dir_all(&install_dir)
.await
.map_err(|e| PluginError::LoadingError(format!("Failed to create plugin directory: {e}")))?;
self.copy_directory(&source_path, &install_dir).await?;
Ok(install_dir)
}
async fn install_from_git(&self, url: &str, name: Option<&str>) -> PluginResult<PathBuf> {
use tempfile::TempDir;
use tokio::process::Command;
let temp_dir = TempDir::new()
.map_err(|e| PluginError::LoadingError(format!("Failed to create temporary directory: {e}")))?;
let temp_path = temp_dir.path();
let output = Command::new("git")
.arg("clone")
.arg(url)
.arg(temp_path)
.output()
.await
.map_err(|e| PluginError::LoadingError(format!("Failed to execute git clone: {e}")))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(PluginError::LoadingError(format!("Git clone failed: {stderr}")));
}
let plugin_name = name
.map(|s| s.to_string())
.unwrap_or_else(|| self.extract_name_from_git_url(url));
let install_dir = self.plugins_dir.join(&plugin_name);
fs::create_dir_all(&install_dir)
.await
.map_err(|e| PluginError::LoadingError(format!("Failed to create plugin directory: {e}")))?;
self.copy_directory(temp_path, &install_dir).await?;
let manifest_path = install_dir.join(".vtcode-plugin/plugin.json");
if !manifest_path.exists() {
return Err(PluginError::ManifestValidationError(
"Plugin manifest not found in cloned repository".to_string(),
));
}
Ok(install_dir)
}
async fn install_from_http(&self, url: &str, name: Option<&str>) -> PluginResult<PathBuf> {
let plugin_name = name.map(|s| s.to_string()).unwrap_or_else(|| self.extract_name_from_url(url));
let install_dir = self.plugins_dir.join(&plugin_name);
fs::create_dir_all(&install_dir)
.await
.map_err(|e| PluginError::LoadingError(format!("Failed to create plugin directory: {e}")))?;
let placeholder_manifest = format!(
r#"{{
"name": "{plugin_name}",
"version": "1.0.0",
"description": "Placeholder for HTTP-installed plugin from {url}"
}}"#
);
let manifest_path = install_dir.join(".vtcode-plugin/plugin.json");
let manifest_parent = manifest_path.parent().ok_or_else(|| {
PluginError::LoadingError("Failed to resolve parent directory for plugin manifest path".to_string())
})?;
fs::create_dir_all(manifest_parent).await?;
fs::write(&manifest_path, placeholder_manifest)
.await
.map_err(|e| PluginError::LoadingError(format!("Failed to create placeholder manifest: {e}")))?;
Ok(install_dir)
}
async fn install_from_marketplace(&self, marketplace_id: &str, name: Option<&str>) -> PluginResult<PathBuf> {
let plugin_name = name
.map(|s| s.to_string())
.unwrap_or_else(|| marketplace_id.split('/').next_back().unwrap_or(marketplace_id).to_string());
let install_dir = self.plugins_dir.join(&plugin_name);
fs::create_dir_all(&install_dir)
.await
.map_err(|e| PluginError::LoadingError(format!("Failed to create plugin directory: {e}")))?;
let placeholder_manifest = format!(
r#"{{
"name": "{plugin_name}",
"version": "1.0.0",
"description": "Placeholder for marketplace-installed plugin from {marketplace_id}"
}}"#
);
let manifest_path = install_dir.join(".vtcode-plugin/plugin.json");
let manifest_parent = manifest_path.parent().ok_or_else(|| {
PluginError::LoadingError("Failed to resolve parent directory for plugin manifest path".to_string())
})?;
fs::create_dir_all(manifest_parent).await?;
fs::write(&manifest_path, placeholder_manifest)
.await
.map_err(|e| PluginError::LoadingError(format!("Failed to create placeholder manifest: {e}")))?;
Ok(install_dir)
}
pub async fn uninstall_plugin(&self, plugin_name: &str) -> PluginResult<()> {
let plugin_dir = self.plugins_dir.join(plugin_name);
if !plugin_dir.exists() {
return Err(PluginError::NotFound(plugin_name.to_string().into()));
}
self.runtime.unload_plugin(plugin_name).await?;
fs::remove_dir_all(&plugin_dir)
.await
.map_err(|e| PluginError::LoadingError(format!("Failed to remove plugin directory: {e}")))?;
Ok(())
}
pub async fn list_installed_plugins(&self) -> PluginResult<Vec<String>> {
let mut plugins = Vec::new();
if !self.plugins_dir.exists() {
return Ok(plugins);
}
let mut entries = fs::read_dir(&self.plugins_dir)
.await
.map_err(|e| PluginError::LoadingError(format!("Failed to read plugins directory: {e}")))?;
while let Some(entry) = entries
.next_entry()
.await
.map_err(|e| PluginError::LoadingError(format!("Failed to read directory entry: {e}")))?
{
let path = entry.path();
if path.is_dir() {
let manifest_path = path.join(".vtcode-plugin/plugin.json");
if manifest_path.exists()
&& let Some(name) = path.file_name()
{
plugins.push(name.to_string_lossy().to_string());
}
}
}
Ok(plugins)
}
async fn copy_directory(&self, src: &Path, dst: &Path) -> PluginResult<()> {
Box::pin(async {
if !src.is_dir() {
return Err(PluginError::LoadingError(format!("Source is not a directory: {}", src.display())));
}
fs::create_dir_all(dst)
.await
.map_err(|e| PluginError::LoadingError(format!("Failed to create destination directory: {e}")))?;
let mut entries = fs::read_dir(src)
.await
.map_err(|e| PluginError::LoadingError(format!("Failed to read source directory: {e}")))?;
while let Some(entry) = entries
.next_entry()
.await
.map_err(|e| PluginError::LoadingError(format!("Failed to read directory entry: {e}")))?
{
let src_path = entry.path();
let dst_path = dst.join(entry.file_name());
if src_path.is_dir() {
self.copy_directory(&src_path, &dst_path).await?;
} else {
fs::copy(&src_path, &dst_path)
.await
.map_err(|e| PluginError::LoadingError(format!("Failed to copy file: {e}")))?;
}
}
Ok(())
})
.await
}
fn extract_name_from_git_url(&self, url: &str) -> String {
url.trim_end_matches(".git")
.split('/')
.next_back()
.unwrap_or("unknown-plugin")
.to_string()
}
fn extract_name_from_url(&self, url: &str) -> String {
url.split('/').next_back().unwrap_or("unknown-plugin").to_string()
}
}