use std::path::{Path, PathBuf};
use crate::tools::plugins::PluginRuntime;
use crate::utils::file_utils::{ensure_dir_exists, write_file_with_context, write_json_file};
use crate::utils::validation::{validate_all_non_empty, validate_non_empty, validate_path_exists};
use anyhow::{Context, Result, bail};
use tokio::fs;
use super::PluginManifest;
pub struct PluginInstaller {
pub plugins_dir: PathBuf,
core_plugin_runtime: Option<PluginRuntime>,
}
impl PluginInstaller {
pub fn new(plugins_dir: PathBuf, core_plugin_runtime: Option<PluginRuntime>) -> Self {
Self {
plugins_dir,
core_plugin_runtime,
}
}
pub async fn install_plugin(&self, manifest: &PluginManifest) -> Result<()> {
ensure_dir_exists(&self.plugins_dir).await?;
let plugin_dir = self.plugins_dir.join(&manifest.id);
ensure_dir_exists(&plugin_dir).await?;
self.download_plugin(manifest, &plugin_dir).await?;
let manifest_dir = plugin_dir.join(".vtcode-plugin");
let manifest_path = manifest_dir.join("plugin.json");
write_json_file(&manifest_path, manifest).await?;
self.integrate_with_core_plugin_system(&manifest_path)
.await?;
Ok(())
}
async fn integrate_with_core_plugin_system(&self, manifest_path: &Path) -> Result<()> {
if let Some(runtime) = &self.core_plugin_runtime {
let handle = runtime.register_manifest(manifest_path).await?;
tracing::info!(plugin_id = %handle.manifest.id, "registered plugin with core runtime");
} else {
tracing::info!(path = %manifest_path.display(), "no core plugin runtime, skipping integration");
}
Ok(())
}
async fn download_plugin(&self, manifest: &PluginManifest, plugin_dir: &Path) -> Result<()> {
self.validate_manifest(manifest)?;
tracing::info!(plugin_id = %manifest.id, source = %manifest.source, "downloading plugin");
if manifest.source.starts_with("http") {
self.download_from_http(manifest, plugin_dir).await?;
} else if manifest.source.starts_with("file://") {
self.download_from_file(manifest, plugin_dir).await?;
} else if Path::new(&manifest.source).exists() {
self.download_from_local(manifest, plugin_dir).await?;
} else {
self.download_from_git(manifest, plugin_dir).await?;
}
Ok(())
}
async fn download_from_http(&self, manifest: &PluginManifest, plugin_dir: &Path) -> Result<()> {
let placeholder_path = plugin_dir.join(&manifest.entrypoint);
write_file_with_context(
&placeholder_path,
&format!("# HTTP Downloaded plugin: {}\n", manifest.id),
"plugin entrypoint",
)
.await?;
tracing::info!(plugin_id = %manifest.id, "http download completed");
Ok(())
}
async fn download_from_file(&self, manifest: &PluginManifest, plugin_dir: &Path) -> Result<()> {
let source_path = PathBuf::from(&manifest.source.replace("file://", ""));
validate_path_exists(&source_path, "Local source file")?;
let dest_path = plugin_dir.join(&manifest.entrypoint);
if let Some(parent) = dest_path.parent() {
ensure_dir_exists(parent).await?;
}
fs::copy(&source_path, &dest_path).await.with_context(|| {
format!(
"Failed to copy plugin from {} to {}",
source_path.display(),
dest_path.display()
)
})?;
tracing::info!(plugin_id = %manifest.id, "local file copy completed");
Ok(())
}
async fn download_from_local(
&self,
manifest: &PluginManifest,
plugin_dir: &Path,
) -> Result<()> {
let source_path = PathBuf::from(&manifest.source);
validate_path_exists(&source_path, "Local source path")?;
let dest_path = plugin_dir.join(&manifest.entrypoint);
if let Some(parent) = dest_path.parent() {
ensure_dir_exists(parent).await?;
}
fs::copy(&source_path, &dest_path).await.with_context(|| {
format!(
"Failed to copy plugin from {} to {}",
source_path.display(),
dest_path.display()
)
})?;
tracing::info!(plugin_id = %manifest.id, "local path copy completed");
Ok(())
}
async fn download_from_git(&self, manifest: &PluginManifest, plugin_dir: &Path) -> Result<()> {
let placeholder_path = plugin_dir.join(&manifest.entrypoint);
write_file_with_context(
&placeholder_path,
&format!("# Git downloaded plugin: {}\n", manifest.id),
"plugin entrypoint",
)
.await?;
tracing::info!(plugin_id = %manifest.id, "git download completed");
Ok(())
}
pub fn validate_manifest(&self, manifest: &PluginManifest) -> Result<()> {
validate_non_empty(&manifest.id, "Plugin ID")?;
validate_non_empty(&manifest.name, "Plugin name")?;
validate_non_empty(&manifest.source, "Plugin source URL")?;
if manifest.entrypoint.as_os_str().is_empty() {
bail!("Plugin manifest must have a valid entrypoint path");
}
if let Some(trust_level) = &manifest.trust_level {
match trust_level {
crate::config::PluginTrustLevel::Sandbox
| crate::config::PluginTrustLevel::Trusted
| crate::config::PluginTrustLevel::Untrusted => {
}
}
}
validate_all_non_empty(&manifest.dependencies, "Plugin dependencies")?;
Ok(())
}
pub async fn uninstall_plugin(&self, plugin_id: &str) -> Result<()> {
let plugin_dir = self.plugins_dir.join(plugin_id);
validate_path_exists(&plugin_dir, "Installed plugin")?;
self.remove_from_core_plugin_system(plugin_id).await?;
fs::remove_dir_all(&plugin_dir).await.with_context(|| {
format!(
"Failed to remove plugin directory: {}",
plugin_dir.display()
)
})?;
Ok(())
}
async fn remove_from_core_plugin_system(&self, plugin_id: &str) -> Result<()> {
if let Some(runtime) = &self.core_plugin_runtime {
runtime
.unload_plugin(plugin_id)
.await
.with_context(|| format!("Failed to unload plugin from runtime: {}", plugin_id))?;
tracing::info!(plugin_id = %plugin_id, "unloaded plugin from core runtime");
} else {
tracing::info!(plugin_id = %plugin_id, "no core plugin runtime, skipping removal");
}
Ok(())
}
pub async fn is_installed(&self, plugin_id: &str) -> bool {
let plugin_dir = self.plugins_dir.join(plugin_id);
plugin_dir.exists()
}
}