use hashbrown::HashMap;
use std::path::PathBuf;
use crate::utils::file_utils::{parse_json_with_context, read_json_file};
use crate::utils::http_client;
use anyhow::{Context, Result, bail};
use base64;
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
use super::{MarketplaceId, MarketplaceManifest, PluginManifest};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub enum MarketplaceSource {
GitHub {
id: String,
owner: String,
repo: String,
refspec: Option<String>, },
Git {
id: String,
url: String,
refspec: Option<String>,
},
Local { id: String, path: String },
Remote { id: String, url: String },
}
impl MarketplaceSource {
pub fn id(&self) -> &str {
match self {
MarketplaceSource::GitHub { id, .. } => id,
MarketplaceSource::Git { id, .. } => id,
MarketplaceSource::Local { id, .. } => id,
MarketplaceSource::Remote { id, .. } => id,
}
}
}
pub struct MarketplaceRegistry {
#[allow(dead_code)]
base_dir: PathBuf,
marketplaces: RwLock<HashMap<MarketplaceId, MarketplaceSource>>,
manifest_cache: RwLock<HashMap<MarketplaceId, MarketplaceManifest>>,
}
impl MarketplaceRegistry {
pub fn new(base_dir: PathBuf) -> Self {
Self {
base_dir,
marketplaces: RwLock::new(HashMap::new()),
manifest_cache: RwLock::new(HashMap::new()),
}
}
pub async fn add_marketplace(&self, source: MarketplaceSource) -> Result<()> {
let mut marketplaces = self.marketplaces.write().await;
marketplaces.insert(source.id().to_string(), source);
Ok(())
}
pub async fn remove_marketplace(&self, id: &str) -> Result<()> {
let mut marketplaces = self.marketplaces.write().await;
if marketplaces.remove(id).is_none() {
bail!("Marketplace '{}' not found", id);
}
let mut cache = self.manifest_cache.write().await;
cache.remove(id);
Ok(())
}
pub async fn list_marketplaces(&self) -> Vec<MarketplaceSource> {
let marketplaces = self.marketplaces.read().await;
marketplaces.values().cloned().collect()
}
pub async fn get_marketplace(&self, id: &str) -> Option<MarketplaceSource> {
let marketplaces = self.marketplaces.read().await;
marketplaces.get(id).cloned()
}
pub async fn update_marketplace(&self, id: &str) -> Result<()> {
let source = {
let marketplaces = self.marketplaces.read().await;
marketplaces.get(id).cloned()
};
let source = match source {
Some(s) => s,
None => bail!("Marketplace '{}' not found", id),
};
let manifest = self.fetch_manifest(&source).await?;
let mut cache = self.manifest_cache.write().await;
cache.insert(id.to_string(), manifest);
Ok(())
}
async fn fetch_manifest(&self, source: &MarketplaceSource) -> Result<MarketplaceManifest> {
match source {
MarketplaceSource::GitHub {
owner,
repo,
refspec,
..
} => {
self.fetch_github_manifest(owner, repo, refspec.as_deref())
.await
}
MarketplaceSource::Git { url, refspec, .. } => {
self.fetch_git_manifest(url, refspec.as_deref()).await
}
MarketplaceSource::Local { path, .. } => {
self.fetch_local_manifest(path).await
}
MarketplaceSource::Remote { url, .. } => {
self.fetch_remote_manifest(url).await
}
}
}
async fn fetch_github_manifest(
&self,
owner: &str,
repo: &str,
refspec: Option<&str>,
) -> Result<MarketplaceManifest> {
use serde_json::Value;
let refspec = refspec.unwrap_or("main");
let api_url = format!(
"https://api.github.com/repos/{}/{}/contents/.vtcode-plugin/marketplace.json?ref={}",
owner, repo, refspec
);
let client = http_client::create_client_with_user_agent("vtcode");
let response = client
.get(&api_url)
.header("User-Agent", "vtcode")
.header("Accept", "application/vnd.github.v3+json")
.send()
.await
.with_context(|| {
format!(
"Failed to fetch manifest from GitHub: {}/{} (ref: {})",
owner, repo, refspec
)
})?;
if !response.status().is_success() {
if response.status() == 404 {
bail!(
"Marketplace manifest not found in GitHub repository: {}/{} (ref: {})",
owner,
repo,
refspec
);
} else {
bail!(
"Failed to fetch manifest from GitHub API: HTTP {} - {}",
response.status(),
response.text().await.unwrap_or_default()
);
}
}
let json_response: Value = response.json().await.with_context(|| {
format!("Failed to parse GitHub API response for {}/{}", owner, repo)
})?;
let content_encoded = json_response
.get("content")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("GitHub API response missing content field"))?;
let content_bytes =
base64::Engine::decode(&base64::engine::general_purpose::STANDARD, content_encoded)
.with_context(|| {
format!(
"Failed to decode base64 content from GitHub: {}/{}",
owner, repo
)
})?;
let content = String::from_utf8(content_bytes).with_context(|| {
format!(
"Failed to decode UTF-8 content from GitHub: {}/{}",
owner, repo
)
})?;
parse_json_with_context(&content, &format!("GitHub: {}/{}", owner, repo))
}
async fn fetch_git_manifest(
&self,
url: &str,
refspec: Option<&str>,
) -> Result<MarketplaceManifest> {
use tempfile::TempDir;
use tokio::process::Command;
let temp_dir =
TempDir::new().with_context(|| "Failed to create temporary directory for git clone")?;
let temp_path = temp_dir.path();
let mut git_cmd = Command::new("git");
git_cmd.arg("clone").arg(url).arg(temp_path);
if let Some(refspec) = refspec {
git_cmd.arg("--branch").arg(refspec);
}
let output = git_cmd
.output()
.await
.with_context(|| format!("Failed to execute git clone for {}", url))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("Git clone failed for {}: {}", url, stderr);
}
let manifest_path = temp_path.join(".vtcode-plugin/marketplace.json");
if !manifest_path.exists() {
bail!("Marketplace manifest not found in repository: {}", url);
}
read_json_file(&manifest_path).await
}
async fn fetch_local_manifest(&self, path: &str) -> Result<MarketplaceManifest> {
use std::path::Path;
let manifest_path = Path::new(path).join(".vtcode-plugin/marketplace.json");
read_json_file(&manifest_path).await
}
async fn fetch_remote_manifest(&self, url: &str) -> Result<MarketplaceManifest> {
let client = http_client::create_default_client();
let response = client
.get(url)
.send()
.await
.with_context(|| format!("Failed to fetch remote manifest from {}", url))?;
if !response.status().is_success() {
bail!(
"Failed to fetch remote manifest: HTTP {}",
response.status()
);
}
let content = response
.text()
.await
.with_context(|| format!("Failed to read response body from {}", url))?;
parse_json_with_context(&content, &format!("remote manifest: {}", url))
}
pub async fn get_cached_manifest(&self, id: &str) -> Option<MarketplaceManifest> {
let cache = self.manifest_cache.read().await;
cache.get(id).cloned()
}
pub async fn list_all_plugins(&self) -> Vec<(MarketplaceId, PluginManifest)> {
let mut all_plugins = Vec::new();
let marketplaces = self.list_marketplaces().await;
for marketplace in marketplaces {
if let Some(manifest) = self.get_cached_manifest(marketplace.id()).await {
for plugin in manifest.plugins {
all_plugins.push((marketplace.id().to_string(), plugin));
}
}
}
all_plugins
}
pub async fn find_plugin(&self, plugin_id: &str) -> Option<(MarketplaceId, PluginManifest)> {
let all_plugins = self.list_all_plugins().await;
all_plugins
.into_iter()
.find(|(_, plugin)| plugin.id == plugin_id)
}
}